@deotio/mcp-sigv4-proxy 0.4.1 → 0.4.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/proxy.js +79 -14
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -3,6 +3,8 @@ import { SignatureV4 } from '@smithy/signature-v4';
|
|
|
3
3
|
import { HttpRequest } from '@smithy/protocol-http';
|
|
4
4
|
import { Sha256 } from '@aws-crypto/sha256-js';
|
|
5
5
|
import readline from 'readline';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
const { version: PROXY_VERSION } = createRequire(import.meta.url)('../package.json');
|
|
6
8
|
export const MAX_SSE_BUFFER_BYTES = 1_048_576; // 1 MB
|
|
7
9
|
const DEFAULT_TIMEOUT_MS = 180_000; // 180s, matches AWS proxy
|
|
8
10
|
const DEFAULT_RETRIES = 2;
|
|
@@ -19,7 +21,7 @@ const LOG_LEVEL_ORDER = {
|
|
|
19
21
|
ERROR: 3,
|
|
20
22
|
SILENT: 4,
|
|
21
23
|
};
|
|
22
|
-
let currentLogLevel = '
|
|
24
|
+
let currentLogLevel = 'INFO';
|
|
23
25
|
export function setLogLevel(level) {
|
|
24
26
|
currentLogLevel = level;
|
|
25
27
|
}
|
|
@@ -65,11 +67,17 @@ export function validateEnv() {
|
|
|
65
67
|
const inferred = parseEndpointUrl(url.hostname);
|
|
66
68
|
const region = process.env.AWS_REGION || inferred?.region || 'us-east-1';
|
|
67
69
|
const service = process.env.AWS_SERVICE || inferred?.service || 'bedrock-agentcore';
|
|
68
|
-
// Parse log level
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Parse log level — only override the default if explicitly set
|
|
71
|
+
if (process.env.MCP_LOG_LEVEL) {
|
|
72
|
+
const envLogLevel = process.env.MCP_LOG_LEVEL.toUpperCase();
|
|
73
|
+
if (envLogLevel in LOG_LEVEL_ORDER) {
|
|
74
|
+
setLogLevel(envLogLevel);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
log('WARNING', `unknown MCP_LOG_LEVEL value "${process.env.MCP_LOG_LEVEL}", using default`);
|
|
78
|
+
}
|
|
72
79
|
}
|
|
80
|
+
log('INFO', `v${PROXY_VERSION} starting`);
|
|
73
81
|
// Parse timeout
|
|
74
82
|
const timeoutMs = process.env.MCP_TIMEOUT
|
|
75
83
|
? Number(process.env.MCP_TIMEOUT) * 1000
|
|
@@ -270,8 +278,20 @@ export async function processLine(line, config, signer) {
|
|
|
270
278
|
return;
|
|
271
279
|
const { body, requestId } = input;
|
|
272
280
|
const request = buildHttpRequest(config.url, body);
|
|
281
|
+
let signed;
|
|
282
|
+
try {
|
|
283
|
+
signed = await signer.sign(request);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
log('ERROR', `signing failed (check AWS credentials for profile/env): ${err}`);
|
|
287
|
+
process.stdout.write(JSON.stringify({
|
|
288
|
+
jsonrpc: '2.0',
|
|
289
|
+
id: requestId,
|
|
290
|
+
error: { code: -32000, message: 'Request signing failed — check AWS credentials' },
|
|
291
|
+
}) + '\n');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
273
294
|
try {
|
|
274
|
-
const signed = await signer.sign(request);
|
|
275
295
|
log('DEBUG', `-> POST ${config.url.pathname}`);
|
|
276
296
|
const response = await fetchWithRetry(config.url.toString(), {
|
|
277
297
|
method: 'POST',
|
|
@@ -304,7 +324,7 @@ function syntheticInitializeResult() {
|
|
|
304
324
|
return {
|
|
305
325
|
protocolVersion: '2025-03-26',
|
|
306
326
|
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
307
|
-
serverInfo: { name: 'mcp-sigv4-proxy-warm', version:
|
|
327
|
+
serverInfo: { name: 'mcp-sigv4-proxy-warm', version: PROXY_VERSION },
|
|
308
328
|
};
|
|
309
329
|
}
|
|
310
330
|
export async function warmBackend(config, signer) {
|
|
@@ -339,13 +359,24 @@ export async function warmBackend(config, signer) {
|
|
|
339
359
|
initResponse = null;
|
|
340
360
|
continue;
|
|
341
361
|
}
|
|
342
|
-
|
|
362
|
+
const hint = initResponse.status === 403
|
|
363
|
+
? ' (check IAM permissions for the calling identity)'
|
|
364
|
+
: initResponse.status === 404
|
|
365
|
+
? ' (check MCP_SERVER_URL — endpoint not found)'
|
|
366
|
+
: initResponse.status === 406
|
|
367
|
+
? ' (server rejected request — possibly missing Accept header)'
|
|
368
|
+
: '';
|
|
369
|
+
log('ERROR', `warm: initialize failed with HTTP ${initResponse.status}${hint}`);
|
|
343
370
|
return false;
|
|
344
371
|
}
|
|
345
372
|
catch (err) {
|
|
373
|
+
const isCredentialError = err instanceof Error &&
|
|
374
|
+
(err.message.includes('credential') ||
|
|
375
|
+
err.message.includes('Could not load credentials') ||
|
|
376
|
+
err.message.includes('profile'));
|
|
346
377
|
if (attempt < config.warmRetries) {
|
|
347
378
|
const delay = config.warmRetryDelayMs * Math.pow(2, attempt);
|
|
348
|
-
log('WARNING', `warm: initialize error (${err}), retrying in ${delay}ms`);
|
|
379
|
+
log(isCredentialError ? 'ERROR' : 'WARNING', `warm: initialize ${isCredentialError ? 'credential error' : 'error'} (${err}), retrying in ${delay}ms (${attempt + 1}/${config.warmRetries})`);
|
|
349
380
|
await sleep(Math.min(delay, deadline - Date.now()));
|
|
350
381
|
continue;
|
|
351
382
|
}
|
|
@@ -391,11 +422,41 @@ export async function warmBackend(config, signer) {
|
|
|
391
422
|
const response = await fetchWithTimeout(config.url.toString(), { method: 'POST', headers: signed.headers, body: listBody }, Math.min(config.timeoutMs, Math.max(1000, deadline - Date.now())));
|
|
392
423
|
if (response.ok) {
|
|
393
424
|
const body = await response.text();
|
|
394
|
-
const
|
|
395
|
-
|
|
425
|
+
const ct = response.headers.get('content-type') ?? '';
|
|
426
|
+
let parsed = null;
|
|
427
|
+
if (ct.includes('text/event-stream')) {
|
|
428
|
+
// Extract the first data: line from the SSE stream
|
|
429
|
+
const dataLine = body.split('\n').find((l) => l.startsWith('data: '));
|
|
430
|
+
if (dataLine) {
|
|
431
|
+
try {
|
|
432
|
+
parsed = JSON.parse(dataLine.slice(6).trim());
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
log('WARNING', `warm: ${method} SSE data line is not valid JSON`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
log('WARNING', `warm: ${method} returned SSE but contained no data: lines`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
try {
|
|
444
|
+
parsed = JSON.parse(body);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
log('WARNING', `warm: ${method} response is not valid JSON (content-type: ${ct})`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (parsed?.result) {
|
|
396
451
|
state.cache[method] = parsed.result;
|
|
397
452
|
log('DEBUG', `warm: cached ${method} (${JSON.stringify(parsed.result).length} bytes)`);
|
|
398
453
|
}
|
|
454
|
+
else if (parsed) {
|
|
455
|
+
log('WARNING', `warm: ${method} response had no .result field`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
log('WARNING', `warm: prefetch ${method} failed with HTTP ${response.status}`);
|
|
399
460
|
}
|
|
400
461
|
}
|
|
401
462
|
catch (err) {
|
|
@@ -458,10 +519,12 @@ export async function handleWarmLine(input, ws) {
|
|
|
458
519
|
if (tryWarmResponse(input.body, input.requestId, ws))
|
|
459
520
|
return true;
|
|
460
521
|
// …otherwise wait for warm-up to complete, then try cache again before forwarding
|
|
461
|
-
|
|
522
|
+
log('INFO', `warm: cache miss for ${method}, waiting for warm-up to complete`);
|
|
523
|
+
const warmed = await ws.ready;
|
|
524
|
+
log('INFO', `warm: warm-up ${warmed ? 'succeeded' : 'failed'} — ${method} ${warmed ? 'served from cache' : 'forwarding to backend'}`);
|
|
462
525
|
if (tryWarmResponse(input.body, input.requestId, ws))
|
|
463
526
|
return true;
|
|
464
|
-
|
|
527
|
+
log('INFO', `warm: cache still empty for ${method}, forwarding to backend`);
|
|
465
528
|
}
|
|
466
529
|
// Non-cacheable methods (tools/call etc): return false to forward normally.
|
|
467
530
|
// fetchWithRetry handles any residual 424s if the backend isn't warm yet.
|
|
@@ -490,7 +553,9 @@ export function startProxy() {
|
|
|
490
553
|
}
|
|
491
554
|
}
|
|
492
555
|
await processLine(line, config, signer);
|
|
493
|
-
}).catch(() => {
|
|
556
|
+
}).catch((err) => {
|
|
557
|
+
log('ERROR', `unhandled error in line processing: ${err}`);
|
|
558
|
+
});
|
|
494
559
|
});
|
|
495
560
|
rl.on('close', async () => {
|
|
496
561
|
log('INFO', 'stdin closed, draining in-flight requests');
|
package/package.json
CHANGED