@phidiassj/aiyoperps-mcp-bridge 0.8.4 → 0.8.6

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
@@ -2,6 +2,10 @@
2
2
 
3
3
  This package exposes a standard MCP `stdio` server that forwards requests to the local AiyoPerps HTTP MCP endpoint.
4
4
 
5
+ It supports both:
6
+ - classic `Content-Length` framed MCP stdio
7
+ - newline-delimited JSON messages used by current Codex MCP startup
8
+
5
9
  ## Usage
6
10
 
7
11
  ```bash
@@ -14,6 +18,8 @@ Custom endpoint:
14
18
  npx -y @phidiassj/aiyoperps-mcp-bridge --url http://127.0.0.1:5078/mcp
15
19
  ```
16
20
 
21
+ Use `--url` whenever your MCP endpoint is not the local default.
22
+
17
23
  Or via environment variable:
18
24
 
19
25
  ```bash
@@ -32,6 +38,12 @@ Startup validation before entering stdio mode:
32
38
  npx -y @phidiassj/aiyoperps-mcp-bridge --startup-ping --url http://127.0.0.1:5078/mcp
33
39
  ```
34
40
 
41
+ Write local diagnostics to a file:
42
+
43
+ ```bash
44
+ npx -y @phidiassj/aiyoperps-mcp-bridge --debug-log ~/.aiyoperps/mcp-bridge/codex-debug.log --quiet --url http://127.0.0.1:5078/mcp
45
+ ```
46
+
35
47
  ## Typical MCP config
36
48
 
37
49
  ```json
@@ -42,7 +54,9 @@ npx -y @phidiassj/aiyoperps-mcp-bridge --startup-ping --url http://127.0.0.1:507
42
54
  "args": [
43
55
  "-y",
44
56
  "@phidiassj/aiyoperps-mcp-bridge",
45
- "--startup-ping",
57
+ "--debug-log",
58
+ "~/.aiyoperps/mcp-bridge/codex-debug.log",
59
+ "--quiet",
46
60
  "--url",
47
61
  "http://127.0.0.1:5078/mcp"
48
62
  ]
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
4
6
  const { stdin, stdout, stderr, exit, argv, env } = require('node:process');
5
7
 
6
8
  const options = parseOptions(argv.slice(2));
@@ -8,19 +10,24 @@ const endpoint = options.url;
8
10
  let buffer = Buffer.alloc(0);
9
11
  let expectedBodyLength = null;
10
12
 
13
+ debugTrace(`process started argv=${JSON.stringify(argv.slice(2))} endpoint=${endpoint}`);
14
+
11
15
  if (options.help) {
12
16
  stdout.write(buildHelpText());
13
17
  exit(0);
14
18
  }
15
19
 
16
20
  bootstrap().catch(error => {
21
+ debugTrace(`startup failed error=${error.stack || error.message}`);
17
22
  logError(`[aiyoperps-mcp-bridge] startup failed: ${error.message}`);
18
23
  exit(1);
19
24
  });
20
25
 
21
26
  async function bootstrap() {
22
27
  if (options.healthCheck || options.startupPing) {
28
+ debugTrace(`pingUpstream start healthCheck=${options.healthCheck} startupPing=${options.startupPing}`);
23
29
  await pingUpstream();
30
+ debugTrace('pingUpstream ok');
24
31
  if (options.healthCheck && !options.startupPing) {
25
32
  logInfo(`[aiyoperps-mcp-bridge] health check ok ${endpoint}`);
26
33
  exit(0);
@@ -29,18 +36,22 @@ async function bootstrap() {
29
36
  }
30
37
 
31
38
  logInfo(`[aiyoperps-mcp-bridge] forwarding stdio MCP to ${endpoint}`);
39
+ debugTrace('stdio forwarding active');
32
40
 
33
41
  stdin.on('data', chunk => {
42
+ debugTrace(`stdin data bytes=${chunk.length} preview=${JSON.stringify(truncateForDebug(chunk.toString('utf8'), 240))}`);
34
43
  buffer = Buffer.concat([buffer, chunk]);
35
44
  tryProcessBuffer();
36
45
  });
37
46
 
38
47
  stdin.on('end', () => {
48
+ debugTrace('stdin end');
39
49
  logInfo('[aiyoperps-mcp-bridge] stdin closed');
40
50
  exit(0);
41
51
  });
42
52
 
43
53
  stdin.on('error', error => {
54
+ debugTrace(`stdin error=${error.message}`);
44
55
  logError(`[aiyoperps-mcp-bridge] stdin error: ${error.message}`);
45
56
  exit(1);
46
57
  });
@@ -51,21 +62,38 @@ function tryProcessBuffer() {
51
62
  if (expectedBodyLength === null) {
52
63
  const headerInfo = findHeaderBoundary(buffer);
53
64
  if (!headerInfo) {
54
- return;
65
+ const jsonLineInfo = findJsonLineBoundary(buffer);
66
+ if (!jsonLineInfo) {
67
+ debugTrace(`header boundary not found bufferBytes=${buffer.length} preview=${JSON.stringify(truncateForDebug(buffer.toString('utf8'), 240))}`);
68
+ return;
69
+ }
70
+
71
+ const body = buffer.subarray(0, jsonLineInfo.bodyLength);
72
+ buffer = buffer.subarray(jsonLineInfo.nextOffset);
73
+ debugTrace(`json line parsed bytes=${jsonLineInfo.bodyLength}`);
74
+ handleMessage(body, 'json-line').catch(error => {
75
+ debugTrace(`message handling failed error=${error.stack || error.message}`);
76
+ logError(`[aiyoperps-mcp-bridge] message handling failed: ${error.stack || error.message}`);
77
+ exit(1);
78
+ });
79
+ continue;
55
80
  }
56
81
 
57
82
  const headerText = buffer.subarray(0, headerInfo.index).toString('utf8');
58
83
  const lengthMatch = /content-length:\s*(\d+)/i.exec(headerText);
59
84
  if (!lengthMatch) {
85
+ debugTrace(`invalid frame missing content-length header=${JSON.stringify(truncateForDebug(headerText, 240))}`);
60
86
  logError('[aiyoperps-mcp-bridge] invalid MCP frame: missing Content-Length');
61
87
  exit(1);
62
88
  }
63
89
 
64
90
  expectedBodyLength = Number.parseInt(lengthMatch[1], 10);
91
+ debugTrace(`header parsed separator=${headerInfo.separator} contentLength=${expectedBodyLength}`);
65
92
  buffer = buffer.subarray(headerInfo.bodyOffset);
66
93
  }
67
94
 
68
95
  if (buffer.length < expectedBodyLength) {
96
+ debugTrace(`waiting for body bufferBytes=${buffer.length} expected=${expectedBodyLength}`);
69
97
  return;
70
98
  }
71
99
 
@@ -73,13 +101,32 @@ function tryProcessBuffer() {
73
101
  buffer = buffer.subarray(expectedBodyLength);
74
102
  expectedBodyLength = null;
75
103
 
76
- handleMessage(body).catch(error => {
104
+ handleMessage(body, 'framed').catch(error => {
105
+ debugTrace(`message handling failed error=${error.stack || error.message}`);
77
106
  logError(`[aiyoperps-mcp-bridge] message handling failed: ${error.stack || error.message}`);
78
107
  exit(1);
79
108
  });
80
109
  }
81
110
  }
82
111
 
112
+ function findJsonLineBoundary(sourceBuffer) {
113
+ const newlineIndex = sourceBuffer.indexOf('\n');
114
+ if (newlineIndex === -1) {
115
+ return null;
116
+ }
117
+
118
+ const lineBuffer = sourceBuffer.subarray(0, newlineIndex);
119
+ const lineText = lineBuffer.toString('utf8').trim();
120
+ if (!lineText.startsWith('{') && !lineText.startsWith('[')) {
121
+ return null;
122
+ }
123
+
124
+ return {
125
+ bodyLength: lineBuffer.length,
126
+ nextOffset: newlineIndex + 1
127
+ };
128
+ }
129
+
83
130
  function findHeaderBoundary(sourceBuffer) {
84
131
  const crlfIndex = sourceBuffer.indexOf('\r\n\r\n');
85
132
  const lfIndex = sourceBuffer.indexOf('\n\n');
@@ -91,52 +138,67 @@ function findHeaderBoundary(sourceBuffer) {
91
138
  if (crlfIndex !== -1 && (lfIndex === -1 || crlfIndex <= lfIndex)) {
92
139
  return {
93
140
  index: crlfIndex,
94
- bodyOffset: crlfIndex + 4
141
+ bodyOffset: crlfIndex + 4,
142
+ separator: 'crlf'
95
143
  };
96
144
  }
97
145
 
98
146
  return {
99
147
  index: lfIndex,
100
- bodyOffset: lfIndex + 2
148
+ bodyOffset: lfIndex + 2,
149
+ separator: 'lf'
101
150
  };
102
151
  }
103
152
 
104
- async function handleMessage(bodyBuffer) {
153
+ async function handleMessage(bodyBuffer, outputMode) {
105
154
  const bodyText = bodyBuffer.toString('utf8');
106
155
  let payload;
107
156
 
108
157
  try {
109
158
  payload = JSON.parse(bodyText);
159
+ debugTrace(`client json parsed method=${payload?.method ?? '(none)'} id=${formatIdForDebug(payload?.id)} bytes=${bodyBuffer.length}`);
110
160
  } catch (error) {
161
+ debugTrace(`invalid client json bytes=${bodyBuffer.length} error=${error.message} preview=${JSON.stringify(truncateForDebug(bodyText, 240))}`);
111
162
  logError(`[aiyoperps-mcp-bridge] invalid JSON from client: ${error.message}`);
112
- writeFrame(JSON.stringify({
163
+ writeResponse(JSON.stringify({
113
164
  jsonrpc: '2.0',
114
165
  error: {
115
166
  code: -32700,
116
167
  message: 'Invalid JSON received by bridge.'
117
168
  },
118
169
  id: null
119
- }));
170
+ }), outputMode);
120
171
  return;
121
172
  }
122
173
 
174
+ const shouldRespond = hasResponseId(payload);
175
+
123
176
  let response;
124
177
  try {
125
178
  response = await postJson(endpoint, payload);
179
+ debugTrace(`upstream json ok method=${payload?.method ?? '(none)'} id=${formatIdForDebug(payload?.id)}`);
126
180
  } catch (error) {
181
+ debugTrace(`upstream request failed method=${payload?.method ?? '(none)'} id=${formatIdForDebug(payload?.id)} error=${error.message}`);
127
182
  logError(`[aiyoperps-mcp-bridge] upstream request failed: ${error.message}`);
128
- writeFrame(JSON.stringify({
129
- jsonrpc: '2.0',
130
- error: {
131
- code: -32000,
132
- message: `AiyoPerps MCP endpoint unavailable: ${error.message}`
133
- },
134
- id: payload && typeof payload === 'object' && 'id' in payload ? payload.id : null
135
- }));
183
+ if (shouldRespond) {
184
+ writeResponse(JSON.stringify({
185
+ jsonrpc: '2.0',
186
+ error: {
187
+ code: -32000,
188
+ message: `AiyoPerps MCP endpoint unavailable: ${error.message}`
189
+ },
190
+ id: payload && typeof payload === 'object' && 'id' in payload ? payload.id : null
191
+ }), outputMode);
192
+ }
193
+ return;
194
+ }
195
+
196
+ if (!shouldRespond) {
197
+ debugTrace(`notification handled method=${payload?.method ?? '(none)'} no response`);
136
198
  return;
137
199
  }
138
200
 
139
- writeFrame(JSON.stringify(response));
201
+ writeResponse(JSON.stringify(response), outputMode);
140
202
  }
141
203
 
142
204
  async function postJson(url, payload) {
@@ -174,6 +236,17 @@ async function postJson(url, payload) {
174
236
  }
175
237
  }
176
238
 
239
+ function writeResponse(jsonText, outputMode) {
240
+ if (outputMode === 'json-line') {
241
+ const payload = `${jsonText}\n`;
242
+ debugTrace(`stdout json-line bytes=${Buffer.byteLength(payload, 'utf8')}`);
243
+ stdout.write(payload);
244
+ return;
245
+ }
246
+
247
+ writeFrame(jsonText);
248
+ }
249
+
177
250
  async function pingUpstream() {
178
251
  const response = await postJson(endpoint, {
179
252
  jsonrpc: '2.0',
@@ -191,15 +264,26 @@ async function pingUpstream() {
191
264
  function writeFrame(jsonText) {
192
265
  const body = Buffer.from(jsonText, 'utf8');
193
266
  const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8');
267
+ debugTrace(`stdout frame bytes=${body.length}`);
194
268
  stdout.write(Buffer.concat([header, body]));
195
269
  }
196
270
 
271
+ function hasResponseId(payload) {
272
+ return Boolean(payload) &&
273
+ typeof payload === 'object' &&
274
+ Object.prototype.hasOwnProperty.call(payload, 'id') &&
275
+ payload.id !== undefined &&
276
+ payload.id !== null;
277
+ }
278
+
197
279
  function parseOptions(args) {
280
+ const defaultUrl = env.AIYOPERPS_MCP_URL || 'http://127.0.0.1:5078/mcp';
198
281
  const options = {
199
- url: env.AIYOPERPS_MCP_URL || 'http://127.0.0.1:5078/mcp',
282
+ url: defaultUrl,
200
283
  healthCheck: false,
201
284
  startupPing: false,
202
285
  quiet: false,
286
+ debugLog: '',
203
287
  help: false,
204
288
  timeoutMs: 5000
205
289
  };
@@ -232,6 +316,17 @@ function parseOptions(args) {
232
316
  continue;
233
317
  }
234
318
 
319
+ if (arg.startsWith('--debug-log=')) {
320
+ options.debugLog = arg.slice('--debug-log='.length);
321
+ continue;
322
+ }
323
+
324
+ if (arg === '--debug-log' && index + 1 < args.length) {
325
+ options.debugLog = args[index + 1];
326
+ index += 1;
327
+ continue;
328
+ }
329
+
235
330
  if (arg === '--help' || arg === '-h') {
236
331
  options.help = true;
237
332
  continue;
@@ -269,11 +364,45 @@ function buildHelpText() {
269
364
  ' --startup-ping Ping the upstream MCP endpoint before entering stdio mode',
270
365
  ' --timeout-ms <ms> Request timeout for health/startup ping and upstream calls (default: 5000)',
271
366
  ' --quiet Suppress bridge diagnostic messages on stderr',
367
+ ' --debug-log <path> Append bridge diagnostics to a local log file',
272
368
  ' --help, -h Show this help message',
273
369
  ''
274
370
  ].join('\n');
275
371
  }
276
372
 
373
+ function debugTrace(message) {
374
+ if (!options.debugLog) {
375
+ return;
376
+ }
377
+
378
+ try {
379
+ fs.mkdirSync(path.dirname(options.debugLog), { recursive: true });
380
+ fs.appendFileSync(options.debugLog, `${new Date().toISOString()} pid=${process.pid} ${message}\n`, 'utf8');
381
+ } catch {
382
+ // Do not throw from debug logging.
383
+ }
384
+ }
385
+
386
+ function truncateForDebug(text, maxLength) {
387
+ if (typeof text !== 'string' || text.length <= maxLength) {
388
+ return text;
389
+ }
390
+
391
+ return `${text.slice(0, maxLength)}...`;
392
+ }
393
+
394
+ function formatIdForDebug(id) {
395
+ if (id === undefined || id === null) {
396
+ return '(null)';
397
+ }
398
+
399
+ if (typeof id === 'string') {
400
+ return truncateForDebug(id, 64);
401
+ }
402
+
403
+ return String(id);
404
+ }
405
+
277
406
  function logInfo(message) {
278
407
  if (!options.quiet) {
279
408
  stderr.write(`${message}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phidiassj/aiyoperps-mcp-bridge",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Stdio MCP bridge for the AiyoPerps local HTTP MCP endpoint.",
5
5
  "license": "MIT",
6
6
  "bin": {