aws-sigv4-mcp-proxy
by steel8rat
README.md
# aws-sigv4-mcp-proxy
Bridge a plain-HTTP or stdio [MCP](https://modelcontextprotocol.io) 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`](https://github.com/aws/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
```bash
npm install aws-sigv4-mcp-proxy
```
To use the default AWS credential chain (recommended on ECS / Lambda / AgentCore),
also install the optional peer dependency:
```bash
npm install @aws-sdk/credential-provider-node
```
Requires Node.js ≥ 20 (uses the global `fetch`).
## 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.
```bash
# 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):
```jsonc
{
"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.
```bash
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...
```
```jsonc
{ "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) |
| `--port` / `--host` / `--path` | HTTP listener bind settings (default `127.0.0.1`, ephemeral port, `/mcp`) |
## Programmatic API
### Generic core
```ts
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();
```
```ts
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
```ts
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.
- **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`.
### Limitations
- SSE resumption via `Last-Event-ID` is passed through but the standalone
server-stream does not yet reconnect automatically on drop.
- 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`](https://github.com/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`](https://github.com/zirkelc/aws-signature-v4) | The right signing primitive, but not a proxy |
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues