mcp-secure-remote
v0.0.2
Published
Remote proxy for Model Context Protocol servers with mTLS (mutual TLS) client certificate authentication support.
Maintainers
Readme
mcp-secure-remote
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.
Contents
- 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
- Docker
- Development
- License
What it does
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, …) │◀───────────│ │◀─────────────────│ │
└──────────────┘ └────────────────────┘ └───────────────┘How it works
- 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.
Prerequisites
- 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).
Install
# global
npm install -g mcp-secure-remote
# or ephemeral (recommended for agent configs)
npx mcp-secure-remote <server-url> [options]Generate or obtain client certificates
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.
Quick start
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.pemCLI parameters
Usage: mcp-secure-remote <server-url> [options]
<server-url> is a positional argument (required). Everything else is a
named flag.
General
| 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. |
mTLS / TLS
| 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. |
Parameter rules
--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.
- URLs with embedded credentials, such as
https://user:[email protected]/mcp, are rejected. Use--headeror environment configuration for credentials.
Environment variables
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.
AI agent / IDE integration
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.
Claude Desktop
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.
Claude Code (CLI)
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"
]
}
}
}Cursor
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…"
}
}
}
}Windsurf
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 (VS Code)
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": []
}
}
}Continue (VS Code / JetBrains)
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"
]
}
}
]
}
}Zed
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"
]
}
}
}
}Generic MCP client
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.
Testing your setup
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.
Security notes
- 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. - Redirects are rejected. Outbound transport requests do not follow HTTP redirects, which prevents a remote endpoint from bouncing the client to a different host and reusing the configured TLS credentials there.
- 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.
- Debug logging redacts secrets. The bundled client and proxy avoid
printing TLS passphrases or header values in
--debugoutput. Proxy message tracing logs only JSON-RPC metadata, not full tool arguments or results. - Embedded URL credentials are refused. Userinfo in the remote URL is not accepted because it can leak through process lists and logs.
- Proxy logs to stderr. stdout is reserved for the MCP JSON-RPC stream.
- Client output is terminal-sanitized. Tool names, descriptions, resources, and prompts received from the remote server are escaped before being written to the terminal.
- 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.
Troubleshooting
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.
Docker
A multi-stage Dockerfile is included. The build stage compiles TypeScript;
the runtime stage contains only production dependencies and the compiled
dist/ output — no dev tooling in the final image.
Build the image:
docker build -t mcp-secure-remote .Run the proxy (no mTLS — plain HTTPS server):
docker run -i mcp-secure-remote https://mcp.example.com/mcpThe -i flag is required because the proxy communicates over stdin/stdout.
Run the proxy with mTLS using Docker secrets (recommended for production):
docker run -i \
-e MCP_REMOTE_TLS_CERT=/run/secrets/client.crt \
-e MCP_REMOTE_TLS_KEY=/run/secrets/client.key \
-e MCP_REMOTE_TLS_CA=/run/secrets/ca-bundle.pem \
--mount type=secret,id=client.crt \
--mount type=secret,id=client.key \
--mount type=secret,id=ca-bundle.pem \
mcp-secure-remote https://mcp.example.com/mcpRun the proxy with mTLS by bind-mounting a local cert directory:
docker run -i \
-v /path/to/local/certs:/certs:ro \
-e MCP_REMOTE_TLS_CERT=/certs/client.crt \
-e MCP_REMOTE_TLS_KEY=/certs/client.key \
-e MCP_REMOTE_TLS_CA=/certs/ca-bundle.pem \
mcp-secure-remote https://mcp.example.com/mcpAll TLS configuration is supplied via MCP_REMOTE_TLS_* environment variables
(see Environment variables). No certs are baked into
the image.
Run the client (verify handshake / enumerate server capabilities):
docker run --rm \
-v /path/to/local/certs:/certs:ro \
-e MCP_REMOTE_TLS_CERT=/certs/client.crt \
-e MCP_REMOTE_TLS_KEY=/certs/client.key \
-e MCP_REMOTE_TLS_CA=/certs/ca-bundle.pem \
--entrypoint node mcp-secure-remote dist/client.js \
https://mcp.example.com/mcpPass extra CLI flags by appending them after the image name:
docker run -i mcp-secure-remote \
https://mcp.example.com/mcp \
--transport sse-only \
--tls-min-version TLSv1.3 \
--debugDevelopment
npm install
npm run typecheck
npm run test
npm run test:coverage
npm run build
npm pack --dry-runBuild artifacts land in dist/. dist/proxy.js and dist/client.js are
the two bin entrypoints. npm pack and npm publish rebuild dist/ first via
the package lifecycle scripts.
Test script summary:
npm run test— run all tests once.npm run test:watch— run tests in watch mode.npm run test:coverage— run tests and generate coverage output.npm run test:unit— run tests undertest/unit.
License
MIT — see LICENSE.
