@deotio/mcp-sigv4-proxy 0.4.0 → 0.4.1

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 +41 -14
  2. package/package.json +4 -4
package/dist/proxy.js CHANGED
@@ -294,6 +294,8 @@ export async function processLine(line, config, signer) {
294
294
  const WARM_CACHEABLE = new Set([
295
295
  'initialize', 'tools/list', 'resources/list', 'prompts/list',
296
296
  ]);
297
+ // Exported for testing
298
+ export { syntheticInitializeResult };
297
299
  /**
298
300
  * Synthetic MCP initialize response returned when the backend hasn't responded yet.
299
301
  * Advertises tools/resources/prompts capabilities so Claude Code proceeds to list calls.
@@ -302,7 +304,7 @@ function syntheticInitializeResult() {
302
304
  return {
303
305
  protocolVersion: '2025-03-26',
304
306
  capabilities: { tools: {}, resources: {}, prompts: {} },
305
- serverInfo: { name: 'mcp-sigv4-proxy-warm', version: '0.4.0' },
307
+ serverInfo: { name: 'mcp-sigv4-proxy-warm', version: '0.4.1' },
306
308
  };
307
309
  }
308
310
  export async function warmBackend(config, signer) {
@@ -437,29 +439,54 @@ export function tryWarmResponse(body, requestId, warmState) {
437
439
  // Claude Code sends this too, but in warm mode we intercepted initialize so we handle it.
438
440
  return true;
439
441
  }
442
+ /**
443
+ * Handle a single parsed JSON-RPC message in warm mode.
444
+ * Returns true if the message was served locally (no need to forward to backend).
445
+ */
446
+ export async function handleWarmLine(input, ws) {
447
+ const method = JSON.parse(input.body).method;
448
+ if (method === 'initialize') {
449
+ // Respond IMMEDIATELY — never block on ws.ready here.
450
+ // This is the critical path: Claude Code's 30s timeout applies to this response.
451
+ const result = ws.cache.initialize ?? syntheticInitializeResult();
452
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: input.requestId, result }) + '\n');
453
+ log('DEBUG', `warm: served initialize ${ws.cache.initialize ? 'from cache' : 'synthetic'}`);
454
+ return true;
455
+ }
456
+ 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;
462
+ if (tryWarmResponse(input.body, input.requestId, ws))
463
+ return true;
464
+ // Fall through to forward if warm-up failed or cache still empty
465
+ }
466
+ // Non-cacheable methods (tools/call etc): return false to forward normally.
467
+ // fetchWithRetry handles any residual 424s if the backend isn't warm yet.
468
+ return false;
469
+ }
440
470
  // --- Main entry ---
441
471
  export function startProxy() {
442
472
  const config = validateEnv();
443
473
  const signer = createSigner(config);
444
474
  const rl = readline.createInterface({ input: process.stdin, terminal: false });
445
- // Start warm-up in background if enabled (non-blocking)
446
- let warmState = null;
447
- if (config.warm) {
448
- warmBackend(config, signer).then((state) => { warmState = state; });
449
- }
475
+ // Start warm-up immediately. warmBackend() has no awaits before returning the WarmState
476
+ // object, so this promise resolves in the next microtask — effectively instant. The
477
+ // warmState.ready promise inside it is what takes minutes to resolve.
478
+ const warmStateP = config.warm
479
+ ? warmBackend(config, signer)
480
+ : null;
450
481
  let pending = Promise.resolve();
451
482
  rl.on('line', (line) => {
452
483
  pending = pending.then(async () => {
453
- // In warm mode, try to serve from cache first
454
- if (warmState) {
484
+ if (warmStateP) {
455
485
  const input = parseInputLine(line);
456
486
  if (input) {
457
- // Wait for warm-up to finish before deciding
458
- const warmActive = await warmState.ready;
459
- if (warmActive && tryWarmResponse(input.body, input.requestId, warmState)) {
460
- return; // served from cache
461
- }
462
- // Fall through: warm mode inactive or method not cacheable — forward normally
487
+ const ws = await warmStateP; // resolves almost instantly
488
+ if (await handleWarmLine(input, ws))
489
+ return;
463
490
  }
464
491
  }
465
492
  await processLine(line, config, signer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deotio/mcp-sigv4-proxy",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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": {
@@ -19,16 +19,16 @@
19
19
  "dependencies": {
20
20
  "@aws-sdk/credential-providers": "^3.0.0",
21
21
  "@aws-crypto/sha256-js": "^5.0.0",
22
- "@smithy/protocol-http": "^4.0.0",
22
+ "@smithy/protocol-http": "^5.3.12",
23
23
  "@smithy/signature-v4": "^4.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@types/jest": "^29.0.0",
26
+ "@types/jest": "^30.0.0",
27
27
  "@types/node": "^22.0.0",
28
28
  "@typescript-eslint/eslint-plugin": "^7.0.0",
29
29
  "@typescript-eslint/parser": "^7.0.0",
30
30
  "eslint": "^8.0.0",
31
- "jest": "^29.0.0",
31
+ "jest": "^30.3.0",
32
32
  "prettier": "^3.0.0",
33
33
  "ts-jest": "^29.0.0",
34
34
  "typescript": "^5.0.0"