mcp-multi-bridge 2.3.2 → 2.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 +34 -0
- package/bin.js +68 -1
- package/lib/preset.js +125 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -42,6 +42,40 @@ npm i -g mcp-multi-bridge mcp-multi-login
|
|
|
42
42
|
>
|
|
43
43
|
> 配置目录与 `config.json` 结构不变,无需重新登录。
|
|
44
44
|
|
|
45
|
+
### 首次使用:配好 mcp.json 即可,不用先跑命令
|
|
46
|
+
|
|
47
|
+
配置不存在时,bridge 启动会按预设自动生成,无需先手动执行登录命令:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
"ebs-plus": {
|
|
51
|
+
"command": "mcp-multi-bridge",
|
|
52
|
+
"args": ["--name", "ebs-plus"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
启动后按以下顺序找预设:
|
|
57
|
+
|
|
58
|
+
| 顺序 | 来源 | 说明 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| 1 | `--preset <名>` | 显式指定,优先级最高 |
|
|
61
|
+
| 2 | `--name <名>` 同名匹配 | `--name ebs-plus` → `presets/ebs-plus.json` |
|
|
62
|
+
| 3 | 都没有 | 不生成,按原行为提示运行登录命令 |
|
|
63
|
+
|
|
64
|
+
预设文件来自 `mcp-multi-login` 包(`presets/`),所以**两个包都要安装**。
|
|
65
|
+
预设不在默认位置时用 `MCP_BRIDGE_PRESETS=<目录>` 指定。
|
|
66
|
+
|
|
67
|
+
三条边界:
|
|
68
|
+
|
|
69
|
+
- **已存在的 `config.json` 一律不覆盖**,手改过的内容不会被冲掉
|
|
70
|
+
- 只生成配置,不写 token——首次调用业务工具时仍需登录一次
|
|
71
|
+
- 生成过程只在 stderr 打印一行,不会污染 stdout 的 JSON-RPC 通道
|
|
72
|
+
|
|
73
|
+
用 `MCP_MULTI_HOME` 指定目录时 `--name` 为空,此时只能靠 `--preset` 指定:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
MCP_MULTI_HOME=~/.my-mcp mcp-multi-bridge --preset ebs-plus
|
|
77
|
+
```
|
|
78
|
+
|
|
45
79
|
## 多后端与工具列表
|
|
46
80
|
|
|
47
81
|
`config.json` 里的每个组织都带一个 `path`,指向它在 Higress 上的 MCP 路由。
|
package/bin.js
CHANGED
|
@@ -55,6 +55,7 @@ import { getOrgAuth, getCurrentOrgKey, listLoggedInOrgs } from './lib/token.js';
|
|
|
55
55
|
import { rpc, extractPayload } from './lib/higress.js';
|
|
56
56
|
import { settings, DEFAULT_SETTINGS } from './lib/settings.js';
|
|
57
57
|
import { buildIndex, searchTools, summarize } from './lib/toolindex.js';
|
|
58
|
+
import { ensureConfig, listPresets, resolvePresetsDir } from './lib/preset.js';
|
|
58
59
|
|
|
59
60
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
60
61
|
const require = createRequire(import.meta.url);
|
|
@@ -163,6 +164,59 @@ function pathAllowed(p, allow) {
|
|
|
163
164
|
});
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* 取 --flag 后面的值;没有该参数时返回空字符串
|
|
169
|
+
*/
|
|
170
|
+
function argOf(argv, flag) {
|
|
171
|
+
const i = argv.indexOf(flag);
|
|
172
|
+
return i >= 0 ? String(argv[i + 1] || '').trim() : '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 首次启动时按预设生成 config.json
|
|
177
|
+
*
|
|
178
|
+
* 只写 stderr:stdout 是 JSON-RPC 通道,写一个字都会破坏 MCP 握手。
|
|
179
|
+
* 任何失败都只提示、不退出——服务还能以「仅 login 工具」的形态起来,
|
|
180
|
+
* 比直接崩掉更容易让用户看到原因。
|
|
181
|
+
*/
|
|
182
|
+
function tryInitFromPreset(argv) {
|
|
183
|
+
if (!CONFIG_FILE) return;
|
|
184
|
+
|
|
185
|
+
const preset = argOf(argv, '--preset');
|
|
186
|
+
let r;
|
|
187
|
+
try {
|
|
188
|
+
r = ensureConfig({ configFile: CONFIG_FILE, name: CONFIG_NAME, preset });
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error(`[bridge] 自动初始化配置失败(不影响启动):${e.message}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (r.created) {
|
|
195
|
+
console.error(`[bridge] 已按预设「${r.preset}」初始化配置: ${r.file}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (r.reason === 'exists' || r.reason === 'no_name') return;
|
|
199
|
+
|
|
200
|
+
if (r.reason === 'presets_dir_missing') {
|
|
201
|
+
console.error(
|
|
202
|
+
`[bridge] 未找到预设目录 ${resolvePresetsDir()}\n` +
|
|
203
|
+
' 自动初始化需要 mcp-multi-login 包提供的预设:npm i -g mcp-multi-login'
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (r.reason === 'preset_not_found') {
|
|
208
|
+
const avail = listPresets(resolvePresetsDir());
|
|
209
|
+
console.error(
|
|
210
|
+
`[bridge] 预设「${preset || CONFIG_NAME}」不存在,可用:${avail.join('、') || '(无)'}\n` +
|
|
211
|
+
` 也可以手动创建 ${CONFIG_FILE}`
|
|
212
|
+
);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (r.reason === 'invalid_preset') {
|
|
216
|
+
console.error(`[bridge] 预设「${preset || CONFIG_NAME}」内容不合法(缺 baseUrl 或 orgs),已跳过`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
166
220
|
/**
|
|
167
221
|
* 处理 --version / --help / --status
|
|
168
222
|
*
|
|
@@ -239,6 +293,13 @@ async function handleCli(argv) {
|
|
|
239
293
|
以前没配时会静默用 ~/.mes-mcp-login,导致新系统忘配就读到 MES 的数据。
|
|
240
294
|
现在没配直接报错,不再猜。
|
|
241
295
|
|
|
296
|
+
首次配置(自动生成):
|
|
297
|
+
配置不存在时,启动会按预设复制一份,无需先手动运行登录命令:
|
|
298
|
+
--name ebs-plus 套用 mcp-multi-login 包里的 presets/ebs-plus.json
|
|
299
|
+
--preset <名> 显式指定预设,优先于 --name 同名匹配
|
|
300
|
+
MCP_BRIDGE_PRESETS=<目录> 预设目录不在 login 包默认位置时指定
|
|
301
|
+
已存在的 config.json 一律不覆盖;生成后仍需登录一次才有业务凭据。
|
|
302
|
+
|
|
242
303
|
当前配置目录: ${CONFIG_DIR || '(未配置)'}
|
|
243
304
|
配置文件: ${CONFIG_FILE || '(未配置)'}
|
|
244
305
|
登录文件: ${CONFIG_DIR ? CONFIG_DIR + '/token.json' : '(未配置)'}(由 ${L.loginCommand} 写入)
|
|
@@ -301,7 +362,9 @@ async function handleCli(argv) {
|
|
|
301
362
|
return false;
|
|
302
363
|
}
|
|
303
364
|
|
|
304
|
-
|
|
365
|
+
const ARGV = process.argv.slice(2);
|
|
366
|
+
|
|
367
|
+
if (await handleCli(ARGV)) {
|
|
305
368
|
process.exit(0);
|
|
306
369
|
}
|
|
307
370
|
|
|
@@ -313,6 +376,10 @@ if (!CONFIG_DIR) {
|
|
|
313
376
|
process.exit(1);
|
|
314
377
|
}
|
|
315
378
|
|
|
379
|
+
// 配置不存在时按预设生成,省掉「必须先手动跑一次登录命令」这一步。
|
|
380
|
+
// 放在 handleCli 之后:--help / --version 不该有写文件的副作用。
|
|
381
|
+
tryInitFromPreset(ARGV);
|
|
382
|
+
|
|
316
383
|
function textResult(text, isError = false) {
|
|
317
384
|
return {
|
|
318
385
|
content: [{ type: 'text', text }],
|
package/lib/preset.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preset.js —— 首次启动时按预设生成 config.json
|
|
3
|
+
*
|
|
4
|
+
* 背景:config.json 的唯一所有者是 `mcp-multi-login`(presets/ 在它那边),
|
|
5
|
+
* bridge 只读不写。于是「先配 mcp.json、客户端拉起 bridge」这条路径上没人写配置,
|
|
6
|
+
* 第一次连上只能退化成一个只有 login 工具的服务,用户必须先手动跑一次登录命令。
|
|
7
|
+
*
|
|
8
|
+
* 本模块把这一步收进 bridge:启动时若配置不存在,按预设复制一份。
|
|
9
|
+
*
|
|
10
|
+
* 四条约束:
|
|
11
|
+
* 1. 只在 config.json **不存在**时写,已存在一律不动(不覆盖用户改过的内容)
|
|
12
|
+
* 2. 不猜目录:仍要求显式 --name 或 MCP_MULTI_HOME(静默默认目录曾导致登错环境)
|
|
13
|
+
* 3. preset 文件仍由 login 包提供,本包不复制一份,避免两边漂移
|
|
14
|
+
* 4. 任何失败都不抛出,只返回 reason —— 初始化失败不该让服务起不来
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
/** 本包位于 <node_modules>/mcp-multi-bridge/lib,往上两层是 node_modules */
|
|
22
|
+
const NODE_MODULES = path.resolve(HERE, '..', '..');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* preset 目录查找顺序
|
|
26
|
+
*
|
|
27
|
+
* 1. 环境变量 MCP_BRIDGE_PRESETS —— 自定义目录,或本机没装 login 包时指过去
|
|
28
|
+
* 2. 同级安装的 mcp-multi-login/presets —— 全局安装时两个包是兄弟目录
|
|
29
|
+
*/
|
|
30
|
+
export function resolvePresetsDir() {
|
|
31
|
+
const env = (process.env.MCP_BRIDGE_PRESETS || '').trim();
|
|
32
|
+
if (env) return env;
|
|
33
|
+
return path.join(NODE_MODULES, 'mcp-multi-login', 'presets');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 列出可用的预设名,目录不存在时返回空数组 */
|
|
37
|
+
export function listPresets(dir) {
|
|
38
|
+
try {
|
|
39
|
+
return fs
|
|
40
|
+
.readdirSync(dir)
|
|
41
|
+
.filter((f) => f.endsWith('.json'))
|
|
42
|
+
.map((f) => f.replace(/\.json$/, ''))
|
|
43
|
+
.sort();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 读取并校验一个预设
|
|
51
|
+
*
|
|
52
|
+
* @returns {object|null} 结构不合格时返回 null(宁可不生成,也不写一份坏配置)
|
|
53
|
+
*/
|
|
54
|
+
export function readPreset(dir, name) {
|
|
55
|
+
if (!name) return null;
|
|
56
|
+
try {
|
|
57
|
+
const file = path.join(dir, `${name}.json`);
|
|
58
|
+
if (!fs.existsSync(file)) return null;
|
|
59
|
+
const cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
60
|
+
if (!cfg || typeof cfg !== 'object') return null;
|
|
61
|
+
if (!Array.isArray(cfg.orgs) || !cfg.orgs.length) return null;
|
|
62
|
+
// baseUrl 与 orgs[].path 是后续一切请求的基础,缺了等于不能用
|
|
63
|
+
if (typeof cfg.baseUrl !== 'string' || !cfg.baseUrl.trim()) return null;
|
|
64
|
+
if (!cfg.orgs.every((o) => o && typeof o.key === 'string' && o.key && typeof o.path === 'string' && o.path)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return cfg;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 配置不存在时按预设生成
|
|
75
|
+
*
|
|
76
|
+
* @param {object} opts
|
|
77
|
+
* @param {string} opts.configFile 目标文件(CONFIG_FILE)
|
|
78
|
+
* @param {string} [opts.name] --name 传入的系统名;用 MCP_MULTI_HOME 时为空
|
|
79
|
+
* @param {string} [opts.preset] --preset 显式指定,优先级高于 name
|
|
80
|
+
* @param {string} [opts.presetsDir] 覆盖预设目录(测试用)
|
|
81
|
+
* @returns {{created:boolean, file?:string, preset?:string, reason?:string}}
|
|
82
|
+
* reason 取值:exists / no_name / presets_dir_missing / preset_not_found / invalid_preset
|
|
83
|
+
*/
|
|
84
|
+
export function ensureConfig(opts = {}) {
|
|
85
|
+
const { configFile, name, preset, presetsDir } = opts;
|
|
86
|
+
|
|
87
|
+
if (!configFile) return { created: false, reason: 'no_config_file' };
|
|
88
|
+
if (fs.existsSync(configFile)) return { created: false, reason: 'exists' };
|
|
89
|
+
|
|
90
|
+
// --preset 优先;否则用 --name 同名匹配(presets 的文件名就等于系统名)
|
|
91
|
+
const target = (preset || name || '').trim();
|
|
92
|
+
if (!target) return { created: false, reason: 'no_name' };
|
|
93
|
+
|
|
94
|
+
const dir = presetsDir || resolvePresetsDir();
|
|
95
|
+
if (!fs.existsSync(dir)) return { created: false, reason: 'presets_dir_missing' };
|
|
96
|
+
|
|
97
|
+
const cfg = readPreset(dir, target);
|
|
98
|
+
if (!cfg) {
|
|
99
|
+
// 区分「文件没有」和「文件损坏」,两者的排查方向完全不同
|
|
100
|
+
const file = path.join(dir, `${target}.json`);
|
|
101
|
+
return {
|
|
102
|
+
created: false,
|
|
103
|
+
reason: fs.existsSync(file) ? 'invalid_preset' : 'preset_not_found'
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const targetDir = path.dirname(configFile);
|
|
108
|
+
fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
109
|
+
|
|
110
|
+
// 原子写:客户端可能同时拉起多个实例,直接写会让另一个读到半个 JSON
|
|
111
|
+
const tmp = `${configFile}.tmp-${process.pid}`;
|
|
112
|
+
try {
|
|
113
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 4), { encoding: 'utf8', mode: 0o600 });
|
|
114
|
+
fs.renameSync(tmp, configFile);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
try {
|
|
117
|
+
fs.unlinkSync(tmp);
|
|
118
|
+
} catch (ignored) {
|
|
119
|
+
// 清理临时文件失败不掩盖原始错误
|
|
120
|
+
}
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { created: true, file: configFile, preset: target };
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-multi-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "多后端 MCP 桥接:把网关后多个后端、多个租户的工具集合并成一个 MCP 服务,凭据由登录 CLI 提供",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node bin.js",
|
|
19
19
|
"pack:check": "npm pack --dry-run",
|
|
20
|
-
"test": "node test/toolindex.test.mjs && node test/headers.test.mjs"
|
|
20
|
+
"test": "node test/toolindex.test.mjs && node test/headers.test.mjs && node test/preset.test.mjs"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"mcp",
|