@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.
Files changed (2) hide show
  1. package/dist/proxy.js +79 -14
  2. 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 = 'ERROR';
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
- const envLogLevel = (process.env.MCP_LOG_LEVEL ?? 'ERROR').toUpperCase();
70
- if (envLogLevel in LOG_LEVEL_ORDER) {
71
- setLogLevel(envLogLevel);
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: '0.4.1' },
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
- log('WARNING', `warm: initialize failed with HTTP ${initResponse.status}`);
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 parsed = JSON.parse(body);
395
- if (parsed.result) {
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
- await ws.ready;
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
- // Fall through to forward if warm-up failed or cache still empty
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deotio/mcp-sigv4-proxy",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "stdio MCP proxy with AWS SigV4 signing — connect Claude Code to any IAM-authenticated MCP server using a named AWS profile",
5
5
  "type": "module",
6
6
  "bin": {