Remote proxy for Model Context Protocol servers with mTLS (mutual TLS) client certificate authentication support.
Project description
mcp-secure-remote
A stdio ↔ remote bridge for the Model Context Protocol with first-class mTLS (mutual TLS) client-certificate authentication.
Run directly with uvx — no install step needed:
uvx mcp-secure-remote https://mcp.example.com/mcp \
--tls-cert ./certs/client.crt \
--tls-key ./certs/client.key \
--tls-ca ./certs/ca-bundle.pem
Works with any MCP-capable AI agent or IDE — Claude Desktop, Claude Code, Cursor, Windsurf, Cline, Continue, Zed, and any custom client that speaks the MCP stdio transport.
Contents
- What it does
- How it works
- Prerequisites
- Install
- Docker
- Generate or obtain client certificates
- Quick start
- CLI parameters
- Environment variables
- AI agent / IDE integration
- Testing your setup
- Security notes
- Troubleshooting
- 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, │ │ (uvx, this proxy) │ │ server │
│ Cursor, …) │◀───────────│ │◀─────────────────│ │
└──────────────┘ └────────────────────┘ └───────────────┘
The remote MCP server can be implemented in any language — Python, Go, Rust, Node.js, etc. The proxy only sees HTTPS + JSON-RPC.
How it works
- AI agent launches
mcp-secure-remote(viauvx) as a local subprocess and talks to it over stdio — the transport every MCP client already supports. - Proxy builds an
httpxHTTPS client 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; the 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
- Python ≥ 3.10.
uvinstalled (curl -LsSf https://astral.sh/uv/install.sh | shon macOS/Linux).- 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
uvx runs the package directly from PyPI in an isolated environment — no
explicit install step needed:
uvx mcp-secure-remote --help
To install permanently in a uv-managed tool environment:
uv tool install mcp-secure-remote
mcp-secure-remote --help
Docker
Docker lets you run mcp-secure-remote without installing Python or uv.
The container reads stdio from its parent process, so MCP clients that
spawn subprocesses work exactly the same way — just replace uvx with
docker run.
Dockerfile
FROM python:3.12-slim
RUN pip install --no-cache-dir mcp-secure-remote
ENTRYPOINT ["mcp-secure-remote"]
Build the image:
docker build -t mcp-secure-remote .
Run directly
Mount your cert directory (read-only) and pass the usual flags:
docker run --rm -i \
-v /absolute/path/to/certs:/certs:ro \
mcp-secure-remote \
https://mcp.example.com/mcp \
--tls-cert /certs/client.crt \
--tls-key /certs/client.key \
--tls-ca /certs/ca-bundle.pem
-i keeps stdin open — required because the proxy reads the MCP stream
from the container's stdin. --rm removes the container after it exits.
Using env vars to keep secrets out of command history:
docker run --rm -i \
-v /absolute/path/to/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/mcp
MCP client config
Replace uvx with docker run in any client config. Example for Claude
Desktop / Claude Code / Cursor:
{
"mcpServers": {
"example": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/absolute/path/to/certs:/certs:ro",
"mcp-secure-remote",
"https://mcp.example.com/mcp",
"--tls-cert", "/certs/client.crt",
"--tls-key", "/certs/client.key",
"--tls-ca", "/certs/ca-bundle.pem"
]
}
}
}
To keep secrets out of the config, pass them via -e instead:
{
"mcpServers": {
"example": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/absolute/path/to/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/mcp"
]
}
}
}
Docker Compose
Useful when you want cert mounts and env vars declared once in version control rather than repeated in every client config.
compose.yml:
services:
mcp-proxy:
build: .
stdin_open: true
volumes:
- /absolute/path/to/certs:/certs:ro
environment:
MCP_REMOTE_TLS_CERT: /certs/client.crt
MCP_REMOTE_TLS_KEY: /certs/client.key
MCP_REMOTE_TLS_CA: /certs/ca-bundle.pem
command:
- https://mcp.example.com/mcp
Run once to verify the connection:
docker compose run --rm mcp-proxy
Then point MCP clients at docker compose run --rm mcp-proxy as the
command (with no extra args — env and volume come from compose.yml):
{
"mcpServers": {
"example": {
"command": "docker",
"args": ["compose", "run", "--rm", "mcp-proxy"]
}
}
}
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 -sha256
Configure 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:
uvx mcp-secure-remote https://mcp.example.com/mcp \
--tls-cert ./certs/client.crt \
--tls-key ./certs/client.key \
--tls-ca ./certs/ca-bundle.pem
PKCS#12 bundle:
uvx mcp-secure-remote https://mcp.example.com/mcp \
--tls-pfx ./certs/client.p12 \
--tls-passphrase "$P12_PASSPHRASE" \
--tls-ca ./certs/ca-bundle.pem
Force SSE transport + pin minimum TLS:
uvx 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.pem
CLI 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). |
--version |
boolean | — | Print version and exit. |
-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 | system 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.
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
Use absolute paths for all cert files — 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": "uvx",
"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"
]
}
}
}
Using env vars to keep secrets out of the config file:
{
"mcpServers": {
"example": {
"command": "uvx",
"args": ["mcp-secure-remote", "https://mcp.example.com/mcp"],
"env": {
"MCP_REMOTE_TLS_CERT": "/absolute/path/client.crt",
"MCP_REMOTE_TLS_KEY": "/absolute/path/client.key",
"MCP_REMOTE_TLS_CA": "/absolute/path/ca-bundle.pem"
}
}
}
}
Restart Claude Desktop after editing.
Claude Code (CLI)
Add via claude mcp add or edit ~/.claude.json / project .mcp.json:
claude mcp add example uvx -- 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
Or in .mcp.json:
{
"mcpServers": {
"example": {
"command": "uvx",
"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": "uvx",
"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": "uvx",
"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)
Open the Cline MCP panel → "Configure MCP Servers" or edit
cline_mcp_settings.json from its extension storage directly:
{
"mcpServers": {
"example": {
"command": "uvx",
"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": "uvx",
"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": "uvx",
"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:
command:uvxargs:["mcp-secure-remote", "<server-url>", …tls-flags]- Optional
envblock forMCP_REMOTE_TLS_*variables to keep secrets out of the args array.
Testing your setup
The bundled mcp-secure-remote-client verifies the TLS handshake and
enumerates the server's capabilities — no real agent needed:
uvx mcp-secure-remote-client https://mcp.example.com/mcp \
--tls-cert ./certs/client.crt \
--tls-key ./certs/client.key \
--tls-ca ./certs/ca-bundle.pem
Output: negotiated capabilities + lists of tools, resources, prompts.
Add --debug for per-message tracing.
For a fully local endpoint that actually requires client-certificate authentication, see LOCAL_MTLS_TESTING.md.
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. - 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.
Troubleshooting
CERTIFICATE_VERIFY_FAILED / 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.
Private key malformed or passphrase error
Ensure the key file is PEM-encoded. If encrypted, supply
--tls-passphrase (or MCP_REMOTE_TLS_PASSPHRASE).
TLS 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.
Development
# clone and set up dev environment
git clone https://github.com/framedparadox/mcp-secure-remote.git
cd mcp-secure-remote
uv sync
# run directly from source
uv run mcp-secure-remote --help
uv run mcp-secure-remote-client --help
# typecheck
uv run mypy src/
# build wheel + sdist
uv build
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_secure_remote-0.0.2.tar.gz.
File metadata
- Download URL: mcp_secure_remote-0.0.2.tar.gz
- Upload date:
- Size: 33.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d2d8eb0b71d3433fe5903b2939761d8f0b2bd790d889811e3c1592788d838bd
|
|
| MD5 |
fb4541d428525f709bfd4bdd267cbe38
|
|
| BLAKE2b-256 |
8a338d304358af2f88238707db8fed380e1b25e92d6cf2d3b5967dfa200b685a
|
Provenance
The following attestation bundles were made for mcp_secure_remote-0.0.2.tar.gz:
Publisher:
publish-py.yml on framedparadox/mcp-secure-remote
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_secure_remote-0.0.2.tar.gz -
Subject digest:
2d2d8eb0b71d3433fe5903b2939761d8f0b2bd790d889811e3c1592788d838bd - Sigstore transparency entry: 1513059198
- Sigstore integration time:
-
Permalink:
framedparadox/mcp-secure-remote@c3614db31a06caacf850120b23f4076fd4cf1318 -
Branch / Tag:
refs/tags/uvx.0.0.2 - Owner: https://github.com/framedparadox
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-py.yml@c3614db31a06caacf850120b23f4076fd4cf1318 -
Trigger Event:
release
-
Statement type:
File details
Details for the file mcp_secure_remote-0.0.2-py3-none-any.whl.
File metadata
- Download URL: mcp_secure_remote-0.0.2-py3-none-any.whl
- Upload date:
- Size: 21.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75db7de53f3c004c011866cc12da08f0b471503e33b145fc433d35a6053f3a2d
|
|
| MD5 |
9f69ed69b5dcbc74fb5a1819fe5e70ac
|
|
| BLAKE2b-256 |
06559bd614eefe243dcb44e38390673979e465b14ab3078ae27ebfc9f33b6a73
|
Provenance
The following attestation bundles were made for mcp_secure_remote-0.0.2-py3-none-any.whl:
Publisher:
publish-py.yml on framedparadox/mcp-secure-remote
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_secure_remote-0.0.2-py3-none-any.whl -
Subject digest:
75db7de53f3c004c011866cc12da08f0b471503e33b145fc433d35a6053f3a2d - Sigstore transparency entry: 1513059491
- Sigstore integration time:
-
Permalink:
framedparadox/mcp-secure-remote@c3614db31a06caacf850120b23f4076fd4cf1318 -
Branch / Tag:
refs/tags/uvx.0.0.2 - Owner: https://github.com/framedparadox
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-py.yml@c3614db31a06caacf850120b23f4076fd4cf1318 -
Trigger Event:
release
-
Statement type: