@phidiassj/aiyoperps-mcp-bridge 0.8.1 → 0.8.4

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 CHANGED
@@ -20,6 +20,18 @@ Or via environment variable:
20
20
  AIYOPERPS_MCP_URL=http://127.0.0.1:5078/mcp npx -y @phidiassj/aiyoperps-mcp-bridge
21
21
  ```
22
22
 
23
+ Health check only:
24
+
25
+ ```bash
26
+ npx -y @phidiassj/aiyoperps-mcp-bridge --health-check --url http://127.0.0.1:5078/mcp
27
+ ```
28
+
29
+ Startup validation before entering stdio mode:
30
+
31
+ ```bash
32
+ npx -y @phidiassj/aiyoperps-mcp-bridge --startup-ping --url http://127.0.0.1:5078/mcp
33
+ ```
34
+
23
35
  ## Typical MCP config
24
36
 
25
37
  ```json
@@ -30,6 +42,7 @@ AIYOPERPS_MCP_URL=http://127.0.0.1:5078/mcp npx -y @phidiassj/aiyoperps-mcp-brid
30
42
  "args": [
31
43
  "-y",
32
44
  "@phidiassj/aiyoperps-mcp-bridge",
45
+ "--startup-ping",
33
46
  "--url",
34
47
  "http://127.0.0.1:5078/mcp"
35
48
  ]
@@ -3,44 +3,66 @@
3
3
 
4
4
  const { stdin, stdout, stderr, exit, argv, env } = require('node:process');
5
5
 
6
- const endpoint = resolveEndpoint(argv.slice(2));
6
+ const options = parseOptions(argv.slice(2));
7
+ const endpoint = options.url;
7
8
  let buffer = Buffer.alloc(0);
8
9
  let expectedBodyLength = null;
9
10
 
10
- stderr.write(`[aiyoperps-mcp-bridge] forwarding stdio MCP to ${endpoint}\n`);
11
-
12
- stdin.on('data', chunk => {
13
- buffer = Buffer.concat([buffer, chunk]);
14
- tryProcessBuffer();
15
- });
16
-
17
- stdin.on('end', () => {
18
- stderr.write('[aiyoperps-mcp-bridge] stdin closed\n');
11
+ if (options.help) {
12
+ stdout.write(buildHelpText());
19
13
  exit(0);
20
- });
14
+ }
21
15
 
22
- stdin.on('error', error => {
23
- stderr.write(`[aiyoperps-mcp-bridge] stdin error: ${error.message}\n`);
16
+ bootstrap().catch(error => {
17
+ logError(`[aiyoperps-mcp-bridge] startup failed: ${error.message}`);
24
18
  exit(1);
25
19
  });
26
20
 
21
+ async function bootstrap() {
22
+ if (options.healthCheck || options.startupPing) {
23
+ await pingUpstream();
24
+ if (options.healthCheck && !options.startupPing) {
25
+ logInfo(`[aiyoperps-mcp-bridge] health check ok ${endpoint}`);
26
+ exit(0);
27
+ return;
28
+ }
29
+ }
30
+
31
+ logInfo(`[aiyoperps-mcp-bridge] forwarding stdio MCP to ${endpoint}`);
32
+
33
+ stdin.on('data', chunk => {
34
+ buffer = Buffer.concat([buffer, chunk]);
35
+ tryProcessBuffer();
36
+ });
37
+
38
+ stdin.on('end', () => {
39
+ logInfo('[aiyoperps-mcp-bridge] stdin closed');
40
+ exit(0);
41
+ });
42
+
43
+ stdin.on('error', error => {
44
+ logError(`[aiyoperps-mcp-bridge] stdin error: ${error.message}`);
45
+ exit(1);
46
+ });
47
+ }
48
+
27
49
  function tryProcessBuffer() {
28
50
  while (true) {
29
51
  if (expectedBodyLength === null) {
30
- const headerEndIndex = buffer.indexOf('\r\n\r\n');
31
- if (headerEndIndex === -1) {
52
+ const headerInfo = findHeaderBoundary(buffer);
53
+ if (!headerInfo) {
32
54
  return;
33
55
  }
34
56
 
35
- const headerText = buffer.subarray(0, headerEndIndex).toString('utf8');
57
+ const headerText = buffer.subarray(0, headerInfo.index).toString('utf8');
36
58
  const lengthMatch = /content-length:\s*(\d+)/i.exec(headerText);
37
59
  if (!lengthMatch) {
38
- stderr.write('[aiyoperps-mcp-bridge] invalid MCP frame: missing Content-Length\n');
60
+ logError('[aiyoperps-mcp-bridge] invalid MCP frame: missing Content-Length');
39
61
  exit(1);
40
62
  }
41
63
 
42
64
  expectedBodyLength = Number.parseInt(lengthMatch[1], 10);
43
- buffer = buffer.subarray(headerEndIndex + 4);
65
+ buffer = buffer.subarray(headerInfo.bodyOffset);
44
66
  }
45
67
 
46
68
  if (buffer.length < expectedBodyLength) {
@@ -52,12 +74,33 @@ function tryProcessBuffer() {
52
74
  expectedBodyLength = null;
53
75
 
54
76
  handleMessage(body).catch(error => {
55
- stderr.write(`[aiyoperps-mcp-bridge] message handling failed: ${error.stack || error.message}\n`);
77
+ logError(`[aiyoperps-mcp-bridge] message handling failed: ${error.stack || error.message}`);
56
78
  exit(1);
57
79
  });
58
80
  }
59
81
  }
60
82
 
83
+ function findHeaderBoundary(sourceBuffer) {
84
+ const crlfIndex = sourceBuffer.indexOf('\r\n\r\n');
85
+ const lfIndex = sourceBuffer.indexOf('\n\n');
86
+
87
+ if (crlfIndex === -1 && lfIndex === -1) {
88
+ return null;
89
+ }
90
+
91
+ if (crlfIndex !== -1 && (lfIndex === -1 || crlfIndex <= lfIndex)) {
92
+ return {
93
+ index: crlfIndex,
94
+ bodyOffset: crlfIndex + 4
95
+ };
96
+ }
97
+
98
+ return {
99
+ index: lfIndex,
100
+ bodyOffset: lfIndex + 2
101
+ };
102
+ }
103
+
61
104
  async function handleMessage(bodyBuffer) {
62
105
  const bodyText = bodyBuffer.toString('utf8');
63
106
  let payload;
@@ -65,7 +108,7 @@ async function handleMessage(bodyBuffer) {
65
108
  try {
66
109
  payload = JSON.parse(bodyText);
67
110
  } catch (error) {
68
- stderr.write(`[aiyoperps-mcp-bridge] invalid JSON from client: ${error.message}\n`);
111
+ logError(`[aiyoperps-mcp-bridge] invalid JSON from client: ${error.message}`);
69
112
  writeFrame(JSON.stringify({
70
113
  jsonrpc: '2.0',
71
114
  error: {
@@ -81,7 +124,7 @@ async function handleMessage(bodyBuffer) {
81
124
  try {
82
125
  response = await postJson(endpoint, payload);
83
126
  } catch (error) {
84
- stderr.write(`[aiyoperps-mcp-bridge] upstream request failed: ${error.message}\n`);
127
+ logError(`[aiyoperps-mcp-bridge] upstream request failed: ${error.message}`);
85
128
  writeFrame(JSON.stringify({
86
129
  jsonrpc: '2.0',
87
130
  error: {
@@ -97,23 +140,51 @@ async function handleMessage(bodyBuffer) {
97
140
  }
98
141
 
99
142
  async function postJson(url, payload) {
100
- const response = await fetch(url, {
101
- method: 'POST',
102
- headers: {
103
- 'Content-Type': 'application/json'
104
- },
105
- body: JSON.stringify(payload)
106
- });
107
-
108
- const text = await response.text();
109
- if (!response.ok) {
110
- throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
111
- }
143
+ const controller = new AbortController();
144
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
112
145
 
113
146
  try {
114
- return JSON.parse(text);
147
+ const response = await fetch(url, {
148
+ method: 'POST',
149
+ headers: {
150
+ 'Content-Type': 'application/json'
151
+ },
152
+ body: JSON.stringify(payload),
153
+ signal: controller.signal
154
+ });
155
+
156
+ const text = await response.text();
157
+ if (!response.ok) {
158
+ throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
159
+ }
160
+
161
+ try {
162
+ return JSON.parse(text);
163
+ } catch (error) {
164
+ throw new Error(`Invalid JSON from AiyoPerps MCP endpoint: ${error.message}`);
165
+ }
115
166
  } catch (error) {
116
- throw new Error(`Invalid JSON from AiyoPerps MCP endpoint: ${error.message}`);
167
+ if (error && error.name === 'AbortError') {
168
+ throw new Error(`Request timeout after ${options.timeoutMs}ms`);
169
+ }
170
+
171
+ throw error;
172
+ } finally {
173
+ clearTimeout(timeout);
174
+ }
175
+ }
176
+
177
+ async function pingUpstream() {
178
+ const response = await postJson(endpoint, {
179
+ jsonrpc: '2.0',
180
+ id: 'startup-ping',
181
+ method: 'ping',
182
+ params: {}
183
+ });
184
+
185
+ if (!response || response.error) {
186
+ const message = response?.error?.message || 'Unexpected MCP ping failure.';
187
+ throw new Error(message);
117
188
  }
118
189
  }
119
190
 
@@ -123,19 +194,92 @@ function writeFrame(jsonText) {
123
194
  stdout.write(Buffer.concat([header, body]));
124
195
  }
125
196
 
126
- function resolveEndpoint(args) {
127
- const fallback = env.AIYOPERPS_MCP_URL || 'http://127.0.0.1:5078/mcp';
197
+ function parseOptions(args) {
198
+ const options = {
199
+ url: env.AIYOPERPS_MCP_URL || 'http://127.0.0.1:5078/mcp',
200
+ healthCheck: false,
201
+ startupPing: false,
202
+ quiet: false,
203
+ help: false,
204
+ timeoutMs: 5000
205
+ };
128
206
 
129
207
  for (let index = 0; index < args.length; index += 1) {
130
208
  const arg = args[index];
131
209
  if (arg.startsWith('--url=')) {
132
- return arg.slice('--url='.length);
210
+ options.url = arg.slice('--url='.length);
211
+ continue;
133
212
  }
134
213
 
135
214
  if (arg === '--url' && index + 1 < args.length) {
136
- return args[index + 1];
215
+ options.url = args[index + 1];
216
+ index += 1;
217
+ continue;
218
+ }
219
+
220
+ if (arg === '--health-check') {
221
+ options.healthCheck = true;
222
+ continue;
223
+ }
224
+
225
+ if (arg === '--startup-ping') {
226
+ options.startupPing = true;
227
+ continue;
228
+ }
229
+
230
+ if (arg === '--quiet') {
231
+ options.quiet = true;
232
+ continue;
233
+ }
234
+
235
+ if (arg === '--help' || arg === '-h') {
236
+ options.help = true;
237
+ continue;
238
+ }
239
+
240
+ if (arg.startsWith('--timeout-ms=')) {
241
+ options.timeoutMs = parseTimeout(arg.slice('--timeout-ms='.length), options.timeoutMs);
242
+ continue;
243
+ }
244
+
245
+ if (arg === '--timeout-ms' && index + 1 < args.length) {
246
+ options.timeoutMs = parseTimeout(args[index + 1], options.timeoutMs);
247
+ index += 1;
137
248
  }
138
249
  }
139
250
 
140
- return fallback;
251
+ return options;
252
+ }
253
+
254
+ function parseTimeout(raw, fallback) {
255
+ const value = Number.parseInt(raw, 10);
256
+ return Number.isInteger(value) && value > 0 ? value : fallback;
257
+ }
258
+
259
+ function buildHelpText() {
260
+ return [
261
+ 'AiyoPerps MCP Bridge',
262
+ '',
263
+ 'Usage:',
264
+ ' npx -y @phidiassj/aiyoperps-mcp-bridge [options]',
265
+ '',
266
+ 'Options:',
267
+ ' --url <endpoint> Target HTTP MCP endpoint (default: http://127.0.0.1:5078/mcp)',
268
+ ' --health-check Verify the upstream MCP endpoint with a ping and exit',
269
+ ' --startup-ping Ping the upstream MCP endpoint before entering stdio mode',
270
+ ' --timeout-ms <ms> Request timeout for health/startup ping and upstream calls (default: 5000)',
271
+ ' --quiet Suppress bridge diagnostic messages on stderr',
272
+ ' --help, -h Show this help message',
273
+ ''
274
+ ].join('\n');
275
+ }
276
+
277
+ function logInfo(message) {
278
+ if (!options.quiet) {
279
+ stderr.write(`${message}\n`);
280
+ }
281
+ }
282
+
283
+ function logError(message) {
284
+ stderr.write(`${message}\n`);
141
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phidiassj/aiyoperps-mcp-bridge",
3
- "version": "0.8.1",
3
+ "version": "0.8.4",
4
4
  "description": "Stdio MCP bridge for the AiyoPerps local HTTP MCP endpoint.",
5
5
  "license": "MIT",
6
6
  "bin": {