A stdio ↔ remote bridge for the Model Context Protocol with first-class mTLS (mutual TLS) client-certificate authentication.
Works with any MCP-capable AI agent or IDE — Claude Desktop, Claude Code, Cursor, Windsurf, Cline, Continue, Zed, VS Code MCP extensions, and any custom client that speaks the MCP stdio transport.
- What it does
- How it works
- Prerequisites
- Install
- Generate or obtain client certificates
- Quick start
- CLI parameters
- Environment variables
- AI agent / IDE integration
- Testing your setup
- Security notes
- Troubleshooting
- Development
- License
mcp-secure-remote spawns as a local stdio MCP server and forwards every
JSON-RPC message to a remote MCP server over HTTPS. Every outbound request
carries a client certificate you supply, so the remote server sees a
cryptographically authenticated connection — no OAuth dance, no bearer
tokens on the wire, no shared API keys.
┌──────────────┐ stdio ┌────────────────────┐ HTTPS + mTLS ┌───────────────┐
│ MCP client │───────────▶│ mcp-secure-remote │─────────────────▶│ Remote MCP │
│ (Claude, │ │ (this proxy) │ │ server │
│ Cursor, …) │◀───────────│ │◀─────────────────│ │
└──────────────┘ └────────────────────┘ └───────────────┘
- AI agent launches
mcp-secure-remoteas a local subprocess and talks to it over stdio (the transport every MCP client already supports). - Proxy builds an undici HTTPS dispatcher seeded with your client cert, private key, and trusted CA bundle.
- Proxy opens either a Streamable HTTP or SSE transport to the remote server (configurable). TLS handshake presents the client cert; server validates it before forwarding the MCP session.
- JSON-RPC frames flow bidirectionally. All proxy logging goes to stderr so the stdio channel stays clean.
- Node.js ≥ 18 (for built-in
fetch,undici, and native TLS features). - A client certificate + private key issued by a CA the remote MCP server trusts (or a PKCS#12 bundle containing both).
- The CA bundle used by the remote server, if it is not in your OS trust store (private/corporate CAs almost always need this).
- The remote MCP server URL (typically
https://host/mcporhttps://host/sse).
# global
npm install -g mcp-secure-remote
# or ephemeral (recommended for agent configs)
npx mcp-secure-remote <server-url> [options]If your team already issues client certs, skip this section. For local testing, generate a throw-away CA + client cert pair with OpenSSL:
# CA
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
-keyout ca.key -out ca.crt -subj "/CN=dev-ca"
# client key + CSR
openssl req -newkey rsa:4096 -nodes \
-keyout client.key -out client.csr -subj "/CN=dev-client"
# sign client cert with CA
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt -days 365 -sha256Configure the remote MCP server to require client certs signed by ca.crt.
Point the proxy at client.crt + client.key + the server's CA bundle.
Cert + key pair:
npx mcp-secure-remote https://mcp.example.com/mcp \
--tls-cert ./certs/client.crt \
--tls-key ./certs/client.key \
--tls-ca ./certs/ca-bundle.pemPKCS#12 bundle:
npx mcp-secure-remote https://mcp.example.com/mcp \
--tls-pfx ./certs/client.p12 \
--tls-passphrase "$P12_PASSPHRASE" \
--tls-ca ./certs/ca-bundle.pemForce SSE transport + pin minimum TLS:
npx mcp-secure-remote https://mcp.example.com/sse \
--transport sse-only \
--tls-min-version TLSv1.3 \
--tls-cert ./certs/client.crt \
--tls-key ./certs/client.key \
--tls-ca ./certs/ca-bundle.pemUsage: mcp-secure-remote <server-url> [options]
<server-url> is a positional argument (required). Everything else is a
named flag.
| Flag | Type | Default | Description |
|---|---|---|---|
<server-url> |
string (URL) | — | Required. Remote MCP endpoint. Must be https://… unless --allow-http is set. |
--header "Name: value" |
string (repeatable) | — | Extra HTTP header on every outbound request. Repeat the flag for multiple headers. |
--transport <strategy> |
enum | http-first |
Transport negotiation. One of http-first, sse-first, http-only, sse-only. -first variants try the preferred transport then fall back; -only variants never fall back. |
--allow-http |
boolean | false |
Permit plain http:// URLs. Off by default; mTLS is meaningless over HTTP. |
--debug |
boolean | false |
Verbose logging to stderr (parsed args, per-message trace, transport selection). |
-h, --help |
boolean | — | Print usage and exit. |
| Flag | Type | Default | Description |
|---|---|---|---|
--tls-cert <path> |
path | — | PEM client certificate (leaf, optionally followed by chain intermediates). |
--tls-key <path> |
path | — | PEM private key matching --tls-cert. Must be supplied together with --tls-cert. |
--tls-ca <path> |
path | — | PEM CA bundle used to verify the remote server. Required for private CAs not in the OS trust store. |
--tls-pfx <path> |
path | — | PKCS#12 (.pfx / .p12) bundle. Mutually exclusive with --tls-cert/--tls-key. |
--tls-passphrase <value> |
string | — | Passphrase protecting the private key or PFX bundle. Prefer the env var to keep secrets off the command line. |
--tls-servername <name> |
string | URL hostname | SNI override. Use when the server cert's SAN differs from the URL host (e.g. IP literal, internal DNS). |
--tls-min-version <ver> |
enum | Node default | Minimum TLS version: TLSv1.2 or TLSv1.3. |
--tls-insecure-skip-verify, --tls-no-verify |
boolean | false |
Disable server certificate validation. Dev only. Proxy prints a warning when enabled. |
--tls-certand--tls-keymust appear together.--tls-pfxcannot combine with--tls-cert/--tls-key.--allow-httpis required for anyhttp://URL. Supplying mTLS flags withhttp://triggers a warning (cert is not sent over plain HTTP).- Unknown
--flagscause parse failure with exit code 2. - Argument errors exit with code 2; runtime errors exit with code 1.
Every TLS flag has an env-var fallback so secrets can stay out of shell history and MCP client configs.
| Variable | Equivalent flag | Values |
|---|---|---|
MCP_REMOTE_TLS_CERT |
--tls-cert |
path |
MCP_REMOTE_TLS_KEY |
--tls-key |
path |
MCP_REMOTE_TLS_CA |
--tls-ca |
path |
MCP_REMOTE_TLS_PFX |
--tls-pfx |
path |
MCP_REMOTE_TLS_PASSPHRASE |
--tls-passphrase |
string |
MCP_REMOTE_TLS_SERVERNAME |
--tls-servername |
string |
MCP_REMOTE_TLS_MIN_VERSION |
--tls-min-version |
TLSv1.2 | TLSv1.3 |
MCP_REMOTE_TLS_INSECURE |
--tls-insecure-skip-verify |
1 / true / yes to disable verify |
Precedence: explicit CLI flag overrides env var.
Every agent below launches the proxy as a local stdio MCP server. Pattern is identical — only the config file format differs. Use absolute paths; agents do not inherit your shell's working directory.
File: ~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows).
{
"mcpServers": {
"example": {
"command": "npx",
"args": [
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/absolute/path/client.crt",
"--tls-key", "/absolute/path/client.key",
"--tls-ca", "/absolute/path/ca-bundle.pem"
]
}
}
}Restart Claude Desktop after editing.
Add a server via the claude mcp add command or edit
~/.claude.json / project .mcp.json:
claude mcp add example npx -- mcp-secure-remote \
https://mcp.example.com/mcp \
--tls-cert /absolute/path/client.crt \
--tls-key /absolute/path/client.key \
--tls-ca /absolute/path/ca-bundle.pemOr in .mcp.json:
{
"mcpServers": {
"example": {
"command": "npx",
"args": [
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/absolute/path/client.crt",
"--tls-key", "/absolute/path/client.key",
"--tls-ca", "/absolute/path/ca-bundle.pem"
]
}
}
}File: ~/.cursor/mcp.json (global) or .cursor/mcp.json (per project).
{
"mcpServers": {
"example": {
"command": "npx",
"args": [
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/absolute/path/client.crt",
"--tls-key", "/absolute/path/client.key",
"--tls-ca", "/absolute/path/ca-bundle.pem"
],
"env": {
"MCP_REMOTE_TLS_PASSPHRASE": "…optional…"
}
}
}
}File: ~/.codeium/windsurf/mcp_config.json.
{
"mcpServers": {
"example": {
"command": "npx",
"args": [
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/absolute/path/client.crt",
"--tls-key", "/absolute/path/client.key",
"--tls-ca", "/absolute/path/ca-bundle.pem"
]
}
}
}Cline reads cline_mcp_settings.json from its extension storage. Open
the Cline MCP panel → "Configure MCP Servers" or edit the file directly:
{
"mcpServers": {
"example": {
"command": "npx",
"args": [
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/absolute/path/client.crt",
"--tls-key", "/absolute/path/client.key",
"--tls-ca", "/absolute/path/ca-bundle.pem"
],
"disabled": false,
"autoApprove": []
}
}
}File: ~/.continue/config.json (or config.yaml).
{
"experimental": {
"modelContextProtocolServers": [
{
"transport": {
"type": "stdio",
"command": "npx",
"args": [
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/absolute/path/client.crt",
"--tls-key", "/absolute/path/client.key",
"--tls-ca", "/absolute/path/ca-bundle.pem"
]
}
}
]
}
}File: ~/.config/zed/settings.json.
{
"context_servers": {
"example": {
"command": {
"path": "npx",
"args": [
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/absolute/path/client.crt",
"--tls-key", "/absolute/path/client.key",
"--tls-ca", "/absolute/path/ca-bundle.pem"
]
}
}
}
}Any client that spawns stdio MCP servers works. Required pieces:
command:npx(or absolute path tonode+dist/proxy.js).args:["mcp-secure-remote", "<server-url>", …flags].- Optional
envblock forMCP_REMOTE_TLS_*variables to keep secrets out of the args array.
Bundled mcp-secure-remote-client verifies the TLS handshake and enumerates the
server's capabilities — no real agent needed:
npx mcp-secure-remote-client https://mcp.example.com/mcp \
--tls-cert ./certs/client.crt \
--tls-key ./certs/client.key \
--tls-ca ./certs/ca-bundle.pemOutput: negotiated capabilities + lists of tools, resources, prompts.
Add --debug for per-message tracing.
- HTTPS only by default.
http://URLs are refused unless--allow-httpis explicitly set. Proxy additionally warns when mTLS flags are combined withhttp://because the client cert will not be sent. - Skip-verify prints a warning.
--tls-insecure-skip-verifydisables server certificate validation; intended for local dev loops only. - Prefer env vars for passphrases. Anything on the CLI may leak into process listings, shell history, or agent logs.
- Proxy logs to stderr. stdout is reserved for the MCP JSON-RPC stream.
- No credential persistence. Proxy does not write certs, keys, or tokens to disk.
- Pin TLS 1.3 (
--tls-min-version TLSv1.3) when the server supports it, to avoid downgrade-prone 1.2 cipher suites.
self signed certificate in certificate chain / unable to verify the first certificate
Point --tls-ca at the PEM bundle that signed the remote server's cert.
OS trust store alone is not enough for private CAs.
Hostname/IP does not match certificate's altnames
Set --tls-servername to the SAN the server cert presents.
error:0909006C:PEM routines:get_name:no start line
Private key file malformed or encrypted. If encrypted, supply
--tls-passphrase (or MCP_REMOTE_TLS_PASSPHRASE).
ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE / alert bad certificate
Server rejected your client cert. Check:
- Cert signed by a CA the server trusts.
- Key matches cert:
openssl x509 -noout -modulus -in client.crt | openssl md5vs.openssl rsa -noout -modulus -in client.key | openssl md5. - Intermediate chain present in
--tls-cert.
Agent shows "failed to start server" with no detail. Run the exact same command in a terminal to see stderr. Agents hide subprocess stderr by default.
Remote transport hangs.
Try --transport sse-only or --transport http-only to isolate which
transport the server actually implements. Add --debug.
already started error in mcp-secure-remote-client.
Upgrade — prior versions double-started the transport. Fixed in current
release.
npm install
npm run typecheck
npm run buildBuild artifacts land in dist/. dist/proxy.js and dist/client.js are
the two bin entrypoints.
MIT — see LICENSE.