Skip to main content
Glama
steel8rat

aws-sigv4-mcp-proxy

by steel8rat

aws-sigv4-mcp-proxy

Bridge a plain-HTTP or stdio MCP client to an AWS SigV4-gated MCP endpoint — Amazon Bedrock AgentCore runtimes, or any MCP server behind API Gateway / a Lambda Function URL / IAM auth.

Most MCP clients (Claude Desktop, Cursor, VS Code, Kiro, the GitHub Copilot SDK, …) can only be pointed at a static URL with static headers, or launched as a command subprocess. None of them can compute a SigV4 signature per request, so they cannot talk to an IAM-secured endpoint directly. This package sits in between:

MCP client  ──plain HTTP / stdio──▶  aws-sigv4-mcp-proxy  ──SigV4-signed HTTPS──▶  AWS

It signs every forwarded request with SigV4 using whatever credentials the process already has (env vars, SSO cache, an EC2 / ECS / AgentCore execution role — the standard AWS credential chain), so the client never has to know AWS auth exists.

This is a small, dependency-light TypeScript counterpart to AWS's Python mcp-proxy-for-aws. It exists because embedding the Python tool (boto3 + botocore + uv) into a Node container adds ~250 MB; this package's runtime dependencies are the Smithy SigV4 signer and a SHA-256 implementation (~6 MB unpacked, no AWS SDK client).

Install

npm install aws-sigv4-mcp-proxy

To use the default AWS credential chain (recommended on ECS / Lambda / AgentCore), also install the optional peer dependency:

npm install @aws-sdk/credential-provider-node

Requires Node.js ≥ 20 (uses the global fetch).

Related MCP server: Miftah

CLI — stdio mode

The default mode reads newline-delimited JSON-RPC on stdin, signs and forwards each message over MCP Streamable HTTP, and writes responses (JSON or text/event-stream) back to stdout. This is how most MCP clients expect to launch a server.

# Bedrock AgentCore runtime (service + region are inferred from the ARN)
aws-sigv4-mcp-proxy arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/my_mcp-abcd1234

# Any other SigV4-gated endpoint
aws-sigv4-mcp-proxy --url https://abc123.execute-api.us-east-1.amazonaws.com/prod/mcp \
  --service execute-api --region us-east-1

Example client config (.mcp.json, Claude Desktop, Cursor, VS Code — same shape):

{
  "mcpServers": {
    "my_remote_mcp": {
      "command": "npx",
      "args": [
        "-y", "aws-sigv4-mcp-proxy",
        "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/my_mcp-abcd1234"
      ],
      "env": { "AWS_PROFILE": "my-sso-profile", "AWS_REGION": "us-east-1" }
    }
  }
}

CLI — HTTP listener mode

--http starts a local unauthenticated listener instead. Point a client that only accepts a static URL at it.

aws-sigv4-mcp-proxy --http --port 9100 \
  arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/my_mcp-abcd1234
# [aws-sigv4-mcp-proxy] listening on http://127.0.0.1:9100/mcp -> https://bedrock-agentcore...
{ "my_remote_mcp": { "type": "http", "url": "http://127.0.0.1:9100/mcp" } }

All flags

Flag

Description

<url|runtime-arn>

Positional target: a URL, or an arn:… AgentCore runtime ARN

--url / --runtime-arn

Same as the positional, explicit form

--http

Run a local HTTP listener instead of the stdio bridge

--service

SigV4 service name (default bedrock-agentcore for an ARN; required with --url)

--region

AWS region (default: parsed from the ARN, else $AWS_REGION / $AWS_DEFAULT_REGION)

--qualifier

AgentCore runtime qualifier (default DEFAULT)

--protocol-version

Initial MCP-Protocol-Version header (stdio mode; updated from the initialize result)

--no-server-stream

Do not open a standalone GET SSE stream for server-initiated messages (stdio mode)

--retry-empty-response

Replay a request answered with an empty HTTP 200, up to 3 attempts — see Empty responses

--port / --host / --path

HTTP listener bind settings (default 127.0.0.1, ephemeral port, /mcp)

Programmatic API

Generic core

import { startSigV4Proxy } from "aws-sigv4-mcp-proxy";

const proxy = await startSigV4Proxy({
  targetUrl: "https://abc123.execute-api.us-east-1.amazonaws.com/prod/mcp",
  service: "execute-api",
  region: "us-east-1",
  // credentials?: static creds or a provider; omit for the default chain
  port: 9100,            // optional; default: ephemeral
});

console.log(proxy.url); // http://127.0.0.1:9100/mcp
// ... on shutdown:
await proxy.close();
import { startStdioProxy } from "aws-sigv4-mcp-proxy";

const proxy = await startStdioProxy({
  targetUrl,
  service: "execute-api",
  region: "us-east-1",
});
await proxy.done; // resolves when stdin ends and in-flight messages are flushed

Bedrock AgentCore convenience layer

import { startAgentCoreProxy, startAgentCoreStdioProxy, agentCoreInvocationUrl } from "aws-sigv4-mcp-proxy";

const proxy = await startAgentCoreProxy({
  runtimeArn: process.env.MY_MCP_RUNTIME_ARN!, // region + service inferred
  qualifier: "DEFAULT",                        // optional
  port: 9100,                                  // optional
});

agentCoreInvocationUrl(runtimeArn);
// https://bedrock-agentcore.<region>.amazonaws.com/runtimes/<url-encoded-arn>/invocations?qualifier=DEFAULT

How the AgentCore invoke URL is derived

There is no published AWS REST reference for invoking an AgentCore runtime over signed HTTP. The path shape here is reverse-engineered from AWS's own tooling — the agentcore CLI's Smithy operation table (InvokeAgentRuntime → POST /runtimes/{agentRuntimeArn}/invocations) and its URL builder — and matches what the Python mcp-proxy-for-aws uses. AWS's tooling treats this endpoint as a genuine MCP Streamable HTTP passthrough (raw JSON-RPC body, Mcp-Session-Id / Mcp-Protocol-Version headers), which is what this proxy relies on.

It has been verified end to end against a live runtime. Still: cross-check it if AWS publishes an official reference, and watch for it changing across agentcore CLI versions. Override the pieces you need with region, qualifier, and dnsSuffix (for non-standard partitions), or bypass the convenience layer entirely with startSigV4Proxy + your own targetUrl.

Design notes

  • Single target per instance. One proxy instance maps to exactly one upstream endpoint. It does not route by path or header to multiple targets — run one instance per target. This keeps signing, session handling and error mapping unambiguous, and is deliberate; please don't re-litigate it without a concrete need.

  • Responses are streamed, not buffered. text/event-stream responses (MCP's mechanism for long-running calls and server notifications) are piped through chunk by chunk in both modes.

  • stdio session handling. The bridge tracks Mcp-Session-Id from the first response and the negotiated protocol version from the initialize result, and attaches both to subsequent requests. After initialization it opens a standalone GET SSE stream for server-initiated messages, disabling it silently if the upstream answers 4xx/5xx (e.g. 405 when the server has no such stream).

  • Transport failures are JSON-RPC shaped. An unreachable upstream or a non-2xx response is returned to the client as { "jsonrpc": "2.0", "id": <request id>, "error": { "code": -32001, "message": … } } rather than a bare HTTP failure.

  • Empty responses are reported, not passed through. See below.

  • Header allowlist. Request: content-type, accept (forced to application/json, text/event-stream when missing or */*), mcp-session-id, mcp-protocol-version, last-event-id. Response: content-type, cache-control, mcp-session-id, mcp-protocol-version, www-authenticate. Both lists are overridable via forwardRequestHeaders / forwardResponseHeaders.

Empty responses

An upstream can answer a JSON-RPC request with HTTP 200 and an empty body. MCP clients then report a bare Transport closed, or — over stdio — wait forever. Against Bedrock AgentCore this is how a failed or not-yet-listening container surfaces: the platform masks the container's error as an empty 200.

Reporting (always on). When a request that is owed a response — a POST of a single JSON-RPC request with a method and an id — gets none, the proxy answers with a JSON-RPC error instead of silence:

{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "Upstream returned HTTP 200 with an empty body" } }
  • The HTTP status stays 200, as JSON-RPC carries errors in the body. If the upstream declared text/event-stream, the error is sent as one SSE message event; otherwise as application/json.

  • "Empty" means no non-whitespace byte before the body ends. Only the leading bytes are read to decide this; SSE bodies keep streaming.

  • In stdio mode, an SSE response that ends without a message answering the request produces -32603 "Upstream stream ended without a response".

  • Notifications, client responses, batches, GET streams and DELETE pass an empty 200 through unchanged — for them it is legitimate.

Retry (off by default). retryEmptyResponse: true (or { attempts, backoffMs }, defaults 3 total attempts and 150ms; CLI --retry-empty-response) replays the identical signed request after an empty 200, with exponential, fully jittered backoff, before falling back to the error above. Each retry is logged through onWarn.

WARNING

Retrying trades at-most-once forat-least-once execution. A container that dies while writing its response has already run the tool, so a replay of a write tool applies it twice. MCP has no idempotency key that could make the replay safe (see SEP-1335 in modelcontextprotocol/modelcontextprotocol). The retry also hides container faults that the error would have surfaced — leave it off unless your tools are idempotent, and investigate the upstream either way.

Limitations

  • SSE resumption via Last-Event-ID is passed through but the standalone server-stream does not yet reconnect automatically on drop.

  • In HTTP mode, an SSE body is judged empty only by its bytes: a stream carrying just keep-alive comments or events without a response is passed through, not reported. (stdio mode parses the stream and does report it.)

  • The HTTP listener is unauthenticated; bind it to loopback (the default) and treat it as a local-only shim.

Prior art

Package

Why it doesn't fit

aws/mcp-proxy-for-aws

Python only; boto3/botocore too heavy to embed in a Node container

mcp-proxy (npm)

SSE ↔ stdio bridge, no AWS auth

mcp-remote (npm)

OAuth-based remote MCP proxy, not SigV4

@aws/bedrock-agentcore-sdk-typescript

RuntimeClient only covers WebSocket/shell auth, no signed HTTP invoke

aws-sigv4-fetch

The right signing primitive, but not a proxy

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    A local MCP auth wrapper and credential broker for multi-account workflows, enabling profile switching, secret injection, and policy enforcement for upstream MCP servers.
    121 npm
    4
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A client-side MCP proxy that injects OAuth 2.0 bearer tokens or API keys into MCP requests, enabling MCP clients to connect to OAuth/API-key-protected MCP servers like Amazon Bedrock AgentCore Gateway. It automatically fetches and refreshes credentials using AgentCore Identity or static values.
    MIT No Attribution
  • A
    license
    Not graded
    quality
    A
    maintenance
    Bridge that lets stdio-only MCP clients connect to remote MCP servers with OAuth and other auth support, enabling local clients to use remote, authorized MCP servers.
    28 npm
    54
    MIT