Zuplo

mTLS (Mutual TLS) Authentication for MCP Servers

mTLS moves the credential out of the request and into the connection: the agent presents an X.509 certificate during the TLS handshake, so no header carries a secret.

Method
mTLS
Platform
Any platform
MCP Specification
2026-07-28

How it works

The certificate goes on the HTTP client the transport uses, never in the MCP payload: a tls.Config on an http.Client in Go, an ssl.SSLContext passed as verify in Python, an undici Agent behind a custom fetch in TypeScript. The request itself carries no credential — the server, or the proxy terminating TLS in front of it, knows the caller from the certificate the connection was established with. That is also the limit. No chat or desktop MCP client config exposes a certificate field, so mTLS reaches servers from agents you write and from nothing else.

Best for
Workload-to-workload calls where both ends are yours and a CA already issues certificates
MCP spec
Transport-layer authentication, outside the spec's OAuth profile — which the spec makes OPTIONAL
Works with
Your own agents only; no chat or desktop client config exposes a certificate field
Effort
An afternoon in code, longer if the PKI does not exist yet

The exchange

Each step in the exchange, in order.

  • Your agent holds cert and key
  • MCP server or the proxy in front of it
  1. The exchange happens once per connection, before any MCP message exists. A server that never sends CertificateRequest never sees a certificate, however well the client is configured.

    Your agent to MCP server

    TLS ClientHello SNI: mcp.example.com
  2. MCP server to Your agent

    CertificateRequest acceptable CAs
  3. Your agent to MCP server

    Certificate, CertificateVerify CN=orders-agent, issued by your CA
  4. Inside MCP server

    Verify the chain, then read the subject
  5. Every message on this connection is already authenticated, so a request copied out of a log carries nothing to replay. The cost is that nothing in MCP advertises the requirement — each caller is configured by hand.

    Your agent to MCP server

    POST /mcp initialize — no Authorization header
  6. MCP server to Your agent

    200 OK initialize result — tools listed
The agent opens a TLS connection, the server asks for a certificate, and the agent presents one signed by a CA the server trusts. The server verifies the chain and reads the subject. Only then does the first MCP message go out, and it carries no credential of its own — no 401, no discovery document, no browser step.

Connect your agent

Pick your language and SDK.

Language
SDKMCP Go SDK
Gomain.go
import (
    "context"
    "crypto/tls"
    "net/http"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

// StreamableClientTransport takes an *http.Client, so the certificate
// goes on that client's TLS config. Nothing MCP-specific is involved.
func connect(ctx context.Context) (*mcp.ClientSession, error) {
    pair, err := tls.LoadX509KeyPair("client.pem", "client.key")
    if err != nil {
        return nil, err
    }

    client := mcp.NewClient(
        &mcp.Implementation{Name: "my-agent", Version: "1.0.0"}, nil)

    return client.Connect(ctx, &mcp.StreamableClientTransport{
        Endpoint: "https://mcp.example.com/mcp",
        HTTPClient: &http.Client{Transport: &http.Transport{
            // Set RootCAs too if your CA is not in the system store.
            TLSClientConfig: &tls.Config{
                Certificates: []tls.Certificate{pair},
            },
        }},
    }, nil)
}

When to use something else

Use this when

  • Both ends are workloads you run and a CA already issues their certificates, so there is no shared secret in an environment variable.
  • A network or compliance rule requires a client certificate on every connection, and TLS terminates in a proxy or mesh you control.
  • You want the credential bound to the connection rather than to a header, so nothing replayable appears in a request or a log.

Use something else when

  • A person has to reach this server from a chat client. Claude, ChatGPT, Cursor and VS Code cannot present a certificate you supply, so mTLS is the wrong answer — check this before anything else.
  • The call is made on behalf of a named user. A certificate identifies a workload, and per-user identity needs the spec's OAuth flow.
  • Nobody owns rotation. Expiry fails the handshake, so every agent stops at once and no MCP error says why.

Authorization is OPTIONAL, and implementations using an HTTP-based transport SHOULD rather than MUST conform to the specification's OAuth profile, so a client certificate sits outside the specification rather than in conflict with it. Both live revisions say exactly that, and neither one's standards list mentions TLS client authentication — the 2026-07-28 list reaches OAuth 2.1, RFC 9728, RFC 8707, RFC 6750, RFC 8414, RFC 7591, RFC 9207 and Client ID Metadata Documents and stops there — which is why no client can discover the requirement.

Zuplo

What lands on you next

A certificate answers one question — which workload opened this connection — and it answers it only for workloads you run. What arrives next is about people, tools, and what the server does with the identity.

Six analysts need two of these tools in Claude. They cannot install a certificate.

mcp-oauth-inbound

The inbound method is a policy on the route rather than server code, so the same server can accept an OAuth sign-in against the identity provider you already run. Exactly one such policy per project.

The certificate proves it is our agent. Which tools may our agent call?

mcp-capability-filter-inbound

A static per-route allow-list of tools, prompts and resources. A call to a hidden tool is refused with MethodNotFound before it reaches your server, and a read-only view of the same upstream is a second route.

Our TLS log has a subject and a timestamp. Which tool actually ran?

capability_invocation

Each tool call emits an event carrying the caller, the capability, the upstream, the outcome and the latency, rather than one connection-level line in an access log.

The API behind the server needs its own credential. Do the agents hold that too?

mcp-token-exchange-inbound

No. What the caller presented is stripped and an independent upstream credential is minted per user or per gateway; the spec forbids forwarding an inbound token with a MUST NOT.

All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out.

Common questions

Can Claude, ChatGPT or Cursor connect to a server that requires a client certificate?

No. Claude's connector documentation lists six supported authentication types — OAuth with DCR, OAuth with a Client ID Metadata Document, Anthropic-held client credentials, custom connection, static headers in beta, and none — and a certificate is not among them. Cursor documents url, headers and auth for a remote server; VS Code documents type, url, headers and oauth. OpenAI states plainly that ChatGPT cannot present customer-provided mTLS certificates.

Doesn't ChatGPT already use mTLS with MCP servers?

Yes, in the other direction. OpenAI documents that ChatGPT presents an OpenAI-managed client certificate when establishing TLS connections to MCP servers, and tells you to verify the leaf certificate's SAN dnsName is mtls.prod.connectors.openai.com. That proves the caller is OpenAI's infrastructure, not your user, and you cannot substitute your own certificate. OAuth 2.1 still authenticates the person.

Claude Code documents CLAUDE_CODE_CLIENT_CERT. Does that cover MCP servers?

Treat it as unverified for MCP. Claude Code documents CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY as enterprise network configuration, read at startup and re-read when settings change, and /status shows an mTLS client cert row once the files load. That page discusses these variables in terms of the connection to the API provider and never mentions MCP, and the documented fields for an HTTP MCP server entry are url, headers, headersHelper, timeout and alwaysLoad. Test it against your own server before designing around it.

Does mTLS violate the MCP specification?

No. Authorization is OPTIONAL, and HTTP transports SHOULD rather than MUST conform to the OAuth profile, so transport-layer client authentication is out of scope rather than in conflict. What you give up is discovery: there is no way to advertise that a server expects a certificate, so each caller is configured directly.

Which specification revision applies here?

2026-07-28, the current protocol version. For mTLS it makes no difference. Neither it nor 2025-11-25 describes client certificates, both make authorization OPTIONAL, and both say HTTP transports SHOULD rather than MUST follow the OAuth profile.

Is client certificate authentication the same thing as mTLS?

Yes. mTLS, mutual TLS, two-way SSL and X.509 client certificate authentication all name the same handshake. It never touches the MCP protocol, so support depends only on whether your SDK lets you supply the HTTP client — which the Go, Python and TypeScript SDKs all do.

Can mTLS and OAuth be combined?

They sit at different layers, so yes: terminate mTLS at the edge and run the spec's OAuth flow behind it. RFC 8705 goes further and binds an access token to the client's certificate, but the MCP authorization spec's standards list does not include RFC 8705, so nothing in MCP asks a client to produce a certificate-bound token.

What happens when a certificate expires?

The handshake fails before any MCP message exists. The spec's error table covers 401, 403 and 400, and a TLS failure becomes none of them, so agents report a transport error and the reason is in your server's TLS log rather than in the protocol. Short-lived certificates need automated renewal on both ends.

One policy engine for APIs, AI, and MCP

Put your MCP servers behind a gateway that speaks every identity provider, filters tools per role, and logs every call.