@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 +15 -1
- package/bin/aiyoperps-mcp-bridge.js +146 -17
- package/package.json +1 -1
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
|
-
"--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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:
|
|
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`);
|