@deotio/mcp-sigv4-proxy 0.4.1 → 0.4.3
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 +80 -18
- 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) {
|
|
@@ -454,14 +515,13 @@ export async function handleWarmLine(input, ws) {
|
|
|
454
515
|
return true;
|
|
455
516
|
}
|
|
456
517
|
if (WARM_CACHEABLE.has(method)) {
|
|
457
|
-
// List methods: serve from cache immediately if available
|
|
458
|
-
if
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
await ws.ready;
|
|
518
|
+
// List methods: serve from cache immediately if available — never block on ws.ready.
|
|
519
|
+
// Warm-up is an optimistic pre-fetch; if it hasn't completed yet, fall through and
|
|
520
|
+
// let the backend answer directly. Blocking here would cause Claude Code's 60s
|
|
521
|
+
// tool-discovery timeout to fire before warm-up finishes.
|
|
462
522
|
if (tryWarmResponse(input.body, input.requestId, ws))
|
|
463
523
|
return true;
|
|
464
|
-
|
|
524
|
+
log('INFO', `warm: cache miss for ${method}, forwarding to backend`);
|
|
465
525
|
}
|
|
466
526
|
// Non-cacheable methods (tools/call etc): return false to forward normally.
|
|
467
527
|
// fetchWithRetry handles any residual 424s if the backend isn't warm yet.
|
|
@@ -490,7 +550,9 @@ export function startProxy() {
|
|
|
490
550
|
}
|
|
491
551
|
}
|
|
492
552
|
await processLine(line, config, signer);
|
|
493
|
-
}).catch(() => {
|
|
553
|
+
}).catch((err) => {
|
|
554
|
+
log('ERROR', `unhandled error in line processing: ${err}`);
|
|
555
|
+
});
|
|
494
556
|
});
|
|
495
557
|
rl.on('close', async () => {
|
|
496
558
|
log('INFO', 'stdin closed, draining in-flight requests');
|
package/package.json
CHANGED