@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 +13 -0
- package/bin/aiyoperps-mcp-bridge.js +184 -40
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
31
|
-
if (
|
|
52
|
+
const headerInfo = findHeaderBoundary(buffer);
|
|
53
|
+
if (!headerInfo) {
|
|
32
54
|
return;
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
const headerText = buffer.subarray(0,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
const
|
|
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
|
-
|
|
210
|
+
options.url = arg.slice('--url='.length);
|
|
211
|
+
continue;
|
|
133
212
|
}
|
|
134
213
|
|
|
135
214
|
if (arg === '--url' && index + 1 < args.length) {
|
|
136
|
-
|
|
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
|
|
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
|
}
|