@autolabz/mcp-bridge 1.0.0 → 1.0.2
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/dist/bridge.d.ts +3 -0
- package/dist/bridge.js +27 -0
- package/dist/index.js +145 -20
- package/package.json +1 -1
package/dist/bridge.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare class Bridge {
|
|
|
4
4
|
private ws;
|
|
5
5
|
private mcpClient;
|
|
6
6
|
private reconnectTimer;
|
|
7
|
+
private heartbeatTimer;
|
|
7
8
|
private shouldReconnect;
|
|
8
9
|
constructor(config: BridgeConfig);
|
|
9
10
|
/**
|
|
@@ -17,6 +18,8 @@ export declare class Bridge {
|
|
|
17
18
|
* Rotate the key hash on the cloud relay.
|
|
18
19
|
*/
|
|
19
20
|
rotateKey(newKeyHash: string): void;
|
|
21
|
+
private startHeartbeat;
|
|
22
|
+
private stopHeartbeat;
|
|
20
23
|
/**
|
|
21
24
|
* Gracefully stop the bridge.
|
|
22
25
|
*/
|
package/dist/bridge.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { MCPClient } from './mcp-client.js';
|
|
3
|
+
const HEARTBEAT_INTERVAL = 30_000; // 30 seconds
|
|
3
4
|
export class Bridge {
|
|
4
5
|
config;
|
|
5
6
|
ws = null;
|
|
6
7
|
mcpClient;
|
|
7
8
|
reconnectTimer = null;
|
|
9
|
+
heartbeatTimer = null;
|
|
8
10
|
shouldReconnect = true;
|
|
9
11
|
constructor(config) {
|
|
10
12
|
this.config = config;
|
|
@@ -38,6 +40,7 @@ export class Bridge {
|
|
|
38
40
|
clearTimeout(this.reconnectTimer);
|
|
39
41
|
this.reconnectTimer = null;
|
|
40
42
|
}
|
|
43
|
+
this.startHeartbeat();
|
|
41
44
|
});
|
|
42
45
|
this.ws.on('message', async (data) => {
|
|
43
46
|
try {
|
|
@@ -53,6 +56,11 @@ export class Bridge {
|
|
|
53
56
|
else if (msg.type === 'error') {
|
|
54
57
|
console.error(`❌ Cloud error: ${msg.message}`);
|
|
55
58
|
}
|
|
59
|
+
else if (msg.type === 'pong') {
|
|
60
|
+
// Heartbeat response received
|
|
61
|
+
// You could update a "lastActivity" timestamp here if you wanted
|
|
62
|
+
// to implement a watchdog that reconnects if the server is silent for too long.
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
catch (err) {
|
|
58
66
|
console.error('❌ Failed to handle cloud message:', err);
|
|
@@ -60,10 +68,12 @@ export class Bridge {
|
|
|
60
68
|
});
|
|
61
69
|
this.ws.on('close', (code, reason) => {
|
|
62
70
|
console.log(`🔌 Disconnected from cloud (${code}: ${reason.toString()})`);
|
|
71
|
+
this.stopHeartbeat();
|
|
63
72
|
this.scheduleReconnect();
|
|
64
73
|
});
|
|
65
74
|
this.ws.on('error', (err) => {
|
|
66
75
|
console.error('❌ WebSocket error:', err.message);
|
|
76
|
+
this.stopHeartbeat();
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
79
|
async handleRequest(msg) {
|
|
@@ -119,6 +129,22 @@ export class Bridge {
|
|
|
119
129
|
}));
|
|
120
130
|
this.config.keyHash = newKeyHash;
|
|
121
131
|
}
|
|
132
|
+
startHeartbeat() {
|
|
133
|
+
this.stopHeartbeat();
|
|
134
|
+
// Initial ping
|
|
135
|
+
// this.sendPing(); // Optional: send one immediately
|
|
136
|
+
this.heartbeatTimer = setInterval(() => {
|
|
137
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
138
|
+
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
139
|
+
}
|
|
140
|
+
}, HEARTBEAT_INTERVAL);
|
|
141
|
+
}
|
|
142
|
+
stopHeartbeat() {
|
|
143
|
+
if (this.heartbeatTimer) {
|
|
144
|
+
clearInterval(this.heartbeatTimer);
|
|
145
|
+
this.heartbeatTimer = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
122
148
|
/**
|
|
123
149
|
* Gracefully stop the bridge.
|
|
124
150
|
*/
|
|
@@ -127,6 +153,7 @@ export class Bridge {
|
|
|
127
153
|
if (this.reconnectTimer) {
|
|
128
154
|
clearTimeout(this.reconnectTimer);
|
|
129
155
|
}
|
|
156
|
+
this.stopHeartbeat();
|
|
130
157
|
if (this.ws) {
|
|
131
158
|
this.ws.close(1000, 'bridge stopped');
|
|
132
159
|
}
|
package/dist/index.js
CHANGED
|
@@ -50,10 +50,19 @@ program
|
|
|
50
50
|
};
|
|
51
51
|
saveConfig(config);
|
|
52
52
|
console.log(`\n✅ 配置已保存到: ${getConfigPath()}`);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
console.log(
|
|
56
|
-
console.log(
|
|
53
|
+
// Construct full bridge URL
|
|
54
|
+
const fullBridgeUrl = `${cloudUrl}/mcp/${nodeId}`;
|
|
55
|
+
console.log('\n🎉 Bridge Initialized Successfully!');
|
|
56
|
+
console.log('----------------------------------------');
|
|
57
|
+
console.log(`🌍 Bridge URL: ${fullBridgeUrl}`);
|
|
58
|
+
console.log(`� API Key: ${rawKey}`);
|
|
59
|
+
console.log('----------------------------------------');
|
|
60
|
+
console.log('⚠️ 请妥善保管 API Key,它不会再次显示!');
|
|
61
|
+
console.log(`\n👉 Next steps:`);
|
|
62
|
+
console.log(` 1. Start the bridge:`);
|
|
63
|
+
console.log(` mcp-bridge start`);
|
|
64
|
+
console.log(` 2. Test connection:`);
|
|
65
|
+
console.log(` mcp-bridge client --url ${fullBridgeUrl} --key ${rawKey}`);
|
|
57
66
|
});
|
|
58
67
|
/**
|
|
59
68
|
* start: Connect the bridge to the cloud relay.
|
|
@@ -67,10 +76,15 @@ program
|
|
|
67
76
|
console.error('❌ No configuration found. Run `mcp-bridge init` first.');
|
|
68
77
|
process.exit(1);
|
|
69
78
|
}
|
|
79
|
+
const fullBridgeUrl = `${config.cloudUrl}/mcp/${config.nodeId}`;
|
|
70
80
|
console.log('🚀 Starting MCP Bridge...');
|
|
71
81
|
console.log(` Cloud: ${config.cloudUrl}`);
|
|
72
82
|
console.log(` Pieces: ${config.piecesEndpoint}`);
|
|
73
|
-
console.log(` NodeID: ${config.nodeId}
|
|
83
|
+
console.log(` NodeID: ${config.nodeId}`);
|
|
84
|
+
console.log(` ---------------------------------------------------`);
|
|
85
|
+
console.log(` 🌍 Bridge URL: ${fullBridgeUrl}`);
|
|
86
|
+
console.log(` ---------------------------------------------------`);
|
|
87
|
+
console.log('\n');
|
|
74
88
|
const bridge = new Bridge(config);
|
|
75
89
|
// Graceful shutdown
|
|
76
90
|
process.on('SIGINT', () => {
|
|
@@ -116,23 +130,134 @@ program
|
|
|
116
130
|
console.log('⚠️ 如果 bridge 正在运行,请重启以生效');
|
|
117
131
|
});
|
|
118
132
|
/**
|
|
119
|
-
*
|
|
133
|
+
* client: Interactive client to test remote bridge
|
|
120
134
|
*/
|
|
121
135
|
program
|
|
122
|
-
.command('
|
|
123
|
-
.description('
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
.command('client')
|
|
137
|
+
.description('Connect to a remote bridge and test tools interactively')
|
|
138
|
+
.option('--url <url>', 'Full bridge URL (e.g. https://cloud.com/mcp/{NODE_ID})')
|
|
139
|
+
.option('--key <key>', 'API Key')
|
|
140
|
+
.action(async (opts) => {
|
|
141
|
+
const readline = await import('readline/promises');
|
|
142
|
+
// 1. Get URL
|
|
143
|
+
let baseUrl = opts.url;
|
|
144
|
+
if (!baseUrl) {
|
|
145
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
146
|
+
baseUrl = await rl.question('Bridge URL: ');
|
|
147
|
+
rl.close();
|
|
148
|
+
}
|
|
149
|
+
baseUrl = baseUrl.replace(/\/+$/, '');
|
|
150
|
+
// 2. Get Key (Hidden)
|
|
151
|
+
let key = opts.key;
|
|
152
|
+
if (!key) {
|
|
153
|
+
const { Writable } = await import('node:stream');
|
|
154
|
+
let muted = false;
|
|
155
|
+
const mutableStdout = new Writable({
|
|
156
|
+
write: function (chunk, encoding, callback) {
|
|
157
|
+
if (!muted)
|
|
158
|
+
process.stdout.write(chunk, encoding);
|
|
159
|
+
callback();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const secureRl = readline.createInterface({
|
|
163
|
+
input: process.stdin,
|
|
164
|
+
output: mutableStdout,
|
|
165
|
+
terminal: true
|
|
166
|
+
});
|
|
167
|
+
process.stdout.write('API Key: ');
|
|
168
|
+
muted = true;
|
|
169
|
+
key = await secureRl.question('');
|
|
170
|
+
muted = false;
|
|
171
|
+
secureRl.close();
|
|
172
|
+
process.stdout.write('\n');
|
|
173
|
+
}
|
|
174
|
+
const headers = {
|
|
175
|
+
'Authorization': `Bearer ${key}`,
|
|
176
|
+
'Content-Type': 'application/json'
|
|
177
|
+
};
|
|
178
|
+
console.log(`\n🔌 Connecting to ${baseUrl}...\n`);
|
|
179
|
+
try {
|
|
180
|
+
// Check Status
|
|
181
|
+
process.stdout.write('Checking status... ');
|
|
182
|
+
const statusRes = await fetch(`${baseUrl}/status`, { headers });
|
|
183
|
+
if (!statusRes.ok)
|
|
184
|
+
throw new Error(`Status check failed: ${statusRes.status}`);
|
|
185
|
+
const status = await statusRes.json();
|
|
186
|
+
console.log('✅ Online');
|
|
187
|
+
console.log(status);
|
|
188
|
+
// List Tools
|
|
189
|
+
console.log('\nFetching tools...');
|
|
190
|
+
const toolsRes = await fetch(`${baseUrl}/tools`, { headers });
|
|
191
|
+
if (!toolsRes.ok)
|
|
192
|
+
throw new Error(`List tools failed: ${toolsRes.status}`);
|
|
193
|
+
const toolsData = (await toolsRes.json());
|
|
194
|
+
const tools = toolsData.result?.tools || [];
|
|
195
|
+
if (tools.length === 0) {
|
|
196
|
+
console.log('⚠️ No tools found.');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
console.table(tools.map((t) => ({
|
|
200
|
+
Name: t.name,
|
|
201
|
+
Description: t.description?.slice(0, 50) + (t.description?.length > 50 ? '...' : '')
|
|
202
|
+
})));
|
|
203
|
+
// Interactive Loop
|
|
204
|
+
const rlLoop = readline.createInterface({
|
|
205
|
+
input: process.stdin,
|
|
206
|
+
output: process.stdout
|
|
207
|
+
});
|
|
208
|
+
console.log('\n💡 Enter a tool name to call it, or "exit" to quit.');
|
|
209
|
+
while (true) {
|
|
210
|
+
const name = await rlLoop.question('\n> ');
|
|
211
|
+
if (name.trim() === 'exit')
|
|
212
|
+
break;
|
|
213
|
+
if (!name.trim())
|
|
214
|
+
continue;
|
|
215
|
+
const tool = tools.find((t) => t.name === name.trim());
|
|
216
|
+
if (!tool) {
|
|
217
|
+
console.log(`❌ Tool "${name}" not found.`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
console.log(`\nCalling ${name}...`);
|
|
221
|
+
console.log('Arguments (JSON, optional):');
|
|
222
|
+
console.log(`Schema: ${JSON.stringify(tool.inputSchema, null, 2)}`);
|
|
223
|
+
const argsStr = await rlLoop.question('Enter args ({}): ');
|
|
224
|
+
let args = {};
|
|
225
|
+
try {
|
|
226
|
+
args = argsStr.trim() ? JSON.parse(argsStr) : {};
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
console.log('❌ Invalid JSON arguments');
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
try {
|
|
234
|
+
const callRes = await fetch(`${baseUrl}/call`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers,
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
method: 'tools/call',
|
|
239
|
+
params: { name, arguments: args }
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
if (!callRes.ok) {
|
|
243
|
+
const errText = await callRes.text();
|
|
244
|
+
console.log(`❌ Call failed (${callRes.status}): ${errText}`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const result = await callRes.json();
|
|
248
|
+
console.log(`✅ Success (${Date.now() - startTime}ms)`);
|
|
249
|
+
console.dir(result, { depth: null, colors: true });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
console.log(`❌ Error: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
rlLoop.close();
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
260
|
+
process.exit(1);
|
|
129
261
|
}
|
|
130
|
-
console.log('📋 MCP Bridge Status');
|
|
131
|
-
console.log('-'.repeat(40));
|
|
132
|
-
console.log(`Config: ${getConfigPath()}`);
|
|
133
|
-
console.log(`Cloud: ${config.cloudUrl}`);
|
|
134
|
-
console.log(`Pieces: ${config.piecesEndpoint}`);
|
|
135
|
-
console.log(`NodeID: ${config.nodeId}`);
|
|
136
|
-
console.log(`KeyHash: ${config.keyHash.slice(0, 12)}...`);
|
|
137
262
|
});
|
|
138
263
|
program.parse();
|