@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 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
- console.log(`📋 Node ID: ${nodeId}`);
54
- console.log(`\n📌 远程调用示例:`);
55
- console.log(` curl http://localhost:8787/mcp/${nodeId}/tools \\`);
56
- console.log(` -H "Authorization: Bearer ${rawKey}"`);
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}\n`);
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
- * status: Show current configuration.
133
+ * client: Interactive client to test remote bridge
120
134
  */
121
135
  program
122
- .command('status')
123
- .description('Show current bridge configuration')
124
- .action(() => {
125
- const config = loadConfig();
126
- if (!config) {
127
- console.log('❌ No configuration found. Run `mcp-bridge init` first.');
128
- return;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autolabz/mcp-bridge",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Bridge local Pieces MCP to cloud relay for remote AI agent access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",