@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.
Files changed (2) hide show
  1. package/dist/proxy.js +80 -18
  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) {
@@ -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 (tryWarmResponse(input.body, input.requestId, ws))
459
- return true;
460
- // …otherwise wait for warm-up to complete, then try cache again before forwarding
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
- // Fall through to forward if warm-up failed or cache still empty
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deotio/mcp-sigv4-proxy",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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": {