Proposals · SEP-2752 · Draft

HTTP Message Signing for MCP Client Authentication

Extensions Track · Created 2026-05-19 · Source

Imported from upstream. Proposed as modelcontextprotocol#2752 by @njdawn on 2026-05-19, where it was still open on 2026-09-25. Accepted into omcp on its technical merit. The text is the author's; only the status, the sponsor field, and documentation links were changed on import.

Reviewed 2026-09-25 — not yet accepted. Held as a draft because no prototype a reviewer can run is cited. Acceptance needs a prototype a reviewer can run, with its setup and the data to run it; Final also needs conformance tests and the reference documentation chapter.

Abstract

This SEP defines an optional, additive client-authentication mechanism for MCP based on RFC 9421 HTTP Message Signatures. It enables MCP servers to authenticate clients with cryptographic proof-of-possession of a private key — without requiring the client to ever transmit a long-lived secret over the wire and without modifying any existing OAuth or MCP-Session-Id flow.

This SEP is filed as an Extensions Track proposal per SEP-2133: it is not a core-protocol change. It is the optional, opt-in extension io.modelcontextprotocol/http-message-signatures (home: the ext-auth repository), negotiated through capabilities.extensions, disabled by default, with no change to the core schema. It is complementary to — not a replacement for — OAuth and DPoP (SEP-1932): DPoP for the OAuth-bound path, this extension for the non-OAuth / API-key / wallet-native case.

The proposal is a refinement of the now-dormant SEP-1415, incorporating the technical feedback from that thread:

Motivation

Why MCP needs cryptographic client authentication

Today MCP authenticates clients with bearer tokens (OAuth access tokens, API keys, MCP-Session-Id). A bearer is, by definition, whoever holds the string is the client. That model has well-understood structural weaknesses that the rest of the web stack has been migrating away from:

  1. Secrets are continuously re-exposed to the server. Every request hands the full credential back. A compromised, malicious, or merely curious server endpoint sees the material the client uses to authenticate everywhere it presents that token. Cryptographic authentication inverts this: the private key never leaves the client. The server only ever sees signatures over specific requests — values that cannot be replayed or repurposed.

  2. No protection against man-in-the-middle / intermediary trust. Bearer tokens rely entirely on TLS for confidentiality. Anything that legitimately terminates TLS along the path — CDNs, reverse proxies, corporate egress inspection, logging middleware — sees the bearer in plaintext and can act as the client until rotation. A signed request is verifiable end-to-end: an intermediary can read the request but cannot mint a new signed request without the private key. This is the property mTLS provides at the transport layer, achievable above TLS termination, which is where most production deployments actually need it.

  3. No request integrity. Bearer auth says nothing about the body. A signature over (@method, @target-uri, content-digest, mcp-session-id, mcp-protocol-version) binds the credential to this specific request: changing the body, method, URI, session ID, or protocol version invalidates the signature. Tampering by a compromised proxy or malicious middlebox is detectable rather than silent.

  4. No replay protection. A captured bearer can be replayed indefinitely until rotation, against any endpoint, with any payload. Signatures with created + nonce constrain each signed request to a freshness window (default 300s) and a single use, collapsing the leak blast radius from "until rotated" to single-digit minutes.

  5. No verifiable client identity. The bearer model gives the server no cryptographic way to answer "is this the same client instance I talked to before?" The signature tag (this SEP) and keyid (RFC 9421) give a stable, unforgeable client identifier without requiring a heavyweight OAuth dance — enabling per-client rate limits, scoped permissions, audit logs, and anomaly detection.

  6. Principle of least exposure. Current OAuth flows leave MCP clients in possession of access tokens really intended for server↔resource-server communication. Proof-of-possession lets the spec stop handing clients credentials they didn't need in the first place.

The same arguments drove the web's adoption of WebAuthn over passwords, DPoP over bare bearer tokens in OAuth 2.1, and HTTP Message Signatures in payment and federation protocols. MCP is currently the outlier.

Where this matters most: wallet-based and high-stakes authentication

A growing class of MCP servers brokers access to wallets and other high-value credentials on behalf of agents — signing EVM/Solana transactions, executing swaps, retrieving API keys, performing on-chain transfers, writing to production databases. For these servers the gaps above stop being theoretical:

Proof-of-possession solves all three: the request body is signed, the client is the keyholder, the bearer becomes worthless on its own. As a corollary, the wallet key itself becomes the natural MCP client credential — the agent's authenticator is the same key material it already uses to sign on-chain.

This generalizes to any MCP server fronting non-revocable side effects: payments, on-chain operations, production writes, secret stores. As more financial and agentic infrastructure integrates over MCP (e.g. via Grok and Claude.ai connectors), the spec needs a proof-of-possession story that ships.

Relationship to existing proposals

Proposal Status Mechanism Relationship
SEP-2133 Final Extensions framework This proposal is an Extensions Track extension under 2133 — not a core change
SEP-1415 Dormant RFC 9421 Picks up 1415's mechanism with the technical-feedback fixes, repackaged as an extension
SEP-1932 Open (PR) RFC 9449 (DPoP) Complementary — DPoP for the OAuth-bound path, this for non-OAuth / wallet. No overlap in core
SEP-1036 Final URL elicitation Orthogonal — concerns user data; this concerns client identity
SEP-1372 Open Init-less MCP This extension works in both initialized and init-less modes
ext-auth (OAuth Client Credentials) Final OAuth 2.0 CC Sibling extension in the same repo; this adds PoP for the non-OAuth case

As an opt-in extension this is additive and disabled by default. Servers and clients that do not negotiate it are unaffected.

Specification

1. Conformance language

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as described in BCP 14 when, and only when, they appear in all capitals.

2. Profile of RFC 9421

2.1 Signed components

Every signed MCP request MUST sign the following components (RFC 9421 §2):

Component Source Notes
@method Derived HTTP method
@target-uri Derived Full request URI
content-digest HTTP field SHA-256 of body per RFC 9530. Required even for empty bodies (digest of empty input).
mcp-protocol-version HTTP field MUST be included when present (per SEP-1372 init-less mode).
mcp-session-id HTTP field MUST be included for all post-initialize requests. MUST NOT be included on the initialize request itself or on init-less first requests, since no session is bound yet.

Servers MUST reject signatures whose Signature-Input does not cover all applicable components above.

2.2 Signature parameters

The following RFC 9421 signature parameters MUST be present:

The alg parameter MUST NOT be sent and MUST be ignored if present. The signature algorithm is derived from the public key material (see §2.4), in line with RFC 9421 §7.3.5's guidance against alg-substitution attacks.

2.3 Replay protection

Servers MUST maintain a nonce cache scoped to (tag, time-window) and MUST reject any request whose (tag, nonce) tuple has been seen within 2 × max_clock_skew + freshness_window (default: 660 seconds).

Servers SHOULD NOT cache full signature values; the nonce-based check is sufficient and is constant-size per client.

2.4 Key material

Public keys are conveyed as JWKs per RFC 7517. Implementations MUST support at least the following curve/algorithm pairing:

Implementations MAY additionally support:

The signature algorithm is determined by the JWK kty + crv combination. Servers MUST NOT accept a signature whose computed algorithm does not match the algorithm registered for the presented keyid.

The keyid value MUST equal the JWK Thumbprint (RFC 7638) of the presented JWK; servers MUST reject any mismatch.

2.5 Stable client identity via tag

The tag parameter (RFC 9421 §2.3) carries a stable client identifier. It is opaque to the protocol and MUST be:

This separates identity (the tag) from proof material (the key), letting servers retain audit trails, rate-limit buckets, and per-client policies through key changes.

2.6 Key distribution

Identity model & extensibility

This SEP is opinionated on the wire profile (§2.1–2.5: covered components, the alg prohibition, replay, keyid == JWK Thumbprint, the stable tag) and pluggable on identity and key discovery. How a request is signed and verified is fixed; where the server learns the verifying key — and any richer identity attached to it — is an extensible set of key-resolution methods over that same signing base.

Conforming servers MUST implement the Signature-Agent baseline (§2.6.1) and MAY accept additional registered methods, advertising the set they accept via the capabilities block (§3.1). The design intent is a decentralized default — no central identity authority required — with centrally-managed identity available opt-in for deployments that want it:

Method Identity model Status
Signature-Agent (Web Bot Auth HMS Directory) Decentralized; key self-published MUST (baseline)
cnf in clientInfo Session-bound, decentralized MAY
WIMSE Workload-Identity-Token Centrally-issued workload identity MAY (registered)
AAuth assertion OAuth-extension identity MAY (registered)

Identity beyond "the keyholder" — multi-hop / call-chain attribution to defeat the confused-deputy problem — is out of scope (§"What this SEP does not address"); the stable tag and the signed content-digest are the hooks a PIC / receipt-chain layer (or WIMSE) binds to per actor.

Per draft-meunier-http-message-signatures-directory, clients MAY include a Signature-Agent header on any signed request:

Signature-Agent: "data:application/http-message-signatures-directory+json;base64,<base64url(JWKS)>"

For desktop / local clients that cannot host a JWKS URL, the data: form is REQUIRED. For hosted clients, the URL form (Signature-Agent: "https://client.example/.well-known/http-message-signatures-directory") is permitted.

This is the only key-distribution method that works for init-less first contact (SEP-1372).

2.6.2 cnf in the extension settings (initialize-bound mode)

Clients that perform initialize MAY additionally bind a key by carrying a cnf (confirmation) member in this extension's settings object under capabilities.extensions (§3.1) — not in core clientInfo, so no core schema is touched:

{
  "method": "initialize",
  "params": {
    "capabilities": {
      "extensions": {
        "io.modelcontextprotocol/http-message-signatures": {
          "cnf": {
            "jwk": {
              "kty": "OKP",
              "crv": "Ed25519",
              "x": "3TQjAzEPW-sE81J4eWUuI2ZQCRJKELwdco_cJa7e9pM"
            }
          }
        }
      }
    },
    "clientInfo": { "name": "example-client", "version": "1.0.0" }
  }
}

The initialize request itself MUST be signed with the corresponding private key. The server binds (mcp-session-id, jwk-thumbprint, tag) for the lifetime of the session.

2.6.3 Registered optional methods (WIMSE, AAuth)

Deployments that need centrally-managed or federated identity MAY carry it in the key-resolution slot without changing the signing base:

A server is not required to implement these; it advertises which it accepts via §3.1. In all cases the signing profile (§2.1–2.5) is unchanged — only keyid resolution differs.

2.7 Optional canonical (JCS) content digest

The baseline content-digest (§2.1) is the RFC 9530 digest over the exact request octets. An intermediary that re-serializes the JSON body (key reorder, whitespace, unicode normalization) invalidates that digest even when the payload is semantically unchanged — relevant for receipt / evidence-relay topologies (e.g. draft-hopley-x402-payment-evidence-frame).

For those topologies, servers and clients MAY negotiate an additional covered component carrying a digest computed over the JCS-canonicalized (RFC 8785) payload, advertised + selected via the capabilities block (§3.1) so both sides agree before relying on it. The byte-exact content-digest remains the MUST baseline; the JCS variant is purely additive, for deployments whose path includes re-serializing relays. JCS is not mandated by default — it imports canonicalization edge cases (number/exponent form, unicode) that the common single-hop MCP topology does not need.

3. Server behavior

3.1 Capability advertisement

This extension is negotiated through the capabilities.extensions map defined by SEP-2133 — not through a new core ServerCapabilities field. The server advertises the extension identifier with a settings object in the initialize response; the client does the same in the request:

{
  "capabilities": {
    "extensions": {
      "io.modelcontextprotocol/http-message-signatures": {
        "algorithms": ["ed25519"],
        "requiresSignature": false,
        "keyResolution": ["signature-agent", "cnf"],
        "contentDigest": ["rfc9530"]
      }
    }
  }
}

Settings object (an empty {} is permitted and means "supported, with defaults"):

Per SEP-2133 the extension is disabled by default and requires explicit opt-in. A server that sets requiresSignature: true MUST reject unsigned requests with HTTP 401 Unauthorized and an Accept-Signature header (§3.3); otherwise it MUST gracefully fall back to core bearer/OAuth behavior for clients that do not negotiate the extension.

3.2 Verification flow

On receiving a signed request, the server MUST:

  1. Parse Signature-Input and Signature headers (RFC 9421 §4.2).
  2. Resolve the public key for the keyid:
    • First, consult the session-bound key (if a session exists).
    • Otherwise, parse Signature-Agent.
    • Otherwise, reject with 401.
  3. Verify the keyid equals the JWK Thumbprint of the resolved key.
  4. Verify created is within [now - 300s, now + 60s].
  5. Verify (tag, nonce) has not been seen.
  6. Verify all required components from §2.1 are present in Signature-Input.
  7. Verify the signature cryptographically using the algorithm derived from the key (§2.4).
  8. Record (tag, nonce) in the replay cache before responding.

Failures in steps 1–7 MUST produce HTTP 401 Unauthorized with Accept-Signature (§3.3) and, optionally, a JSON-RPC error body describing which check failed.

3.3 Algorithm and component negotiation via Accept-Signature

When rejecting a signed request for protocol-level reasons (missing component, unsupported algorithm, unsigned-but-required), the server MUST include an Accept-Signature header (RFC 9421 §5.1) describing what it expects:

HTTP/1.1 401 Unauthorized
Accept-Signature: sig1=("@method" "@target-uri" "content-digest" "mcp-protocol-version" "mcp-session-id");created;nonce;tag="mcp-server.example"

This replaces the custom JSON-RPC error envelope proposed in SEP-1415, keeping signature-layer negotiation at the signature layer.

When multiple algorithms are acceptable, the server MUST advertise them via the capabilities mechanism (§3.1) and MAY repeat the Accept-Signature header per algorithm.

3.4 Key rotation

A client rotates its key by sending a signatures/rotateKey request signed with both the old and new keys (two Signature entries: sig-old and sig-new):

{
  "method": "signatures/rotateKey",
  "params": {
    "newKey": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
  }
}

The server MUST verify both signatures, that the tag matches, and that the new keyid equals the new JWK's thumbprint. On success the server replaces the bound key and SHOULD retain audit history.

4. Client behavior

4.1 Signer abstraction

This SEP does not constrain where signing happens. RFC 9421 only requires that the request carries a valid Signature under the public key the server has bound for the client. Conforming client implementations SHOULD treat signing as an abstract operation that can be satisfied by any of the following, interchangeably:

The out-of-process variants are explicitly recommended for deployments where the surrounding agent process executes untrusted LLM-generated code, since the model cannot exfiltrate key material it never held. PoP at the protocol layer does not by itself defend against an attacker who can coerce the agent process to print env vars or shell history; pairing PoP with an out-of-process signer does.

4.2 Requirements

Clients SHOULD:

Clients MUST NOT:

5. Core schema impact: none

As an Extensions Track extension (SEP-2133) this proposal makes no change to the core MCP schema. All negotiation rides the existing capabilities.extensions map (§3.1); the key material (cnf), algorithm set, key-resolution methods, and content-digest profile all live inside this extension's settings object — not in core ClientCapabilities, ServerCapabilities, or Implementation.

The only method the extension introduces is signatures/rotateKey (§3.4). It is scoped to this extension and MUST be routed only when the extension has been negotiated; implementations SHOULD treat it as namespaced to the extension identifier to avoid collisions.

6. Example signed request

POST /mcp HTTP/1.1
Host: server.example.com
Content-Type: application/json
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
MCP-Protocol-Version: 2026-03-26
MCP-Session-Id: sess_abc123
Signature-Agent: "data:application/http-message-signatures-directory+json;base64,eyJrZXlzIjpbey4uLn1dfQ=="
Signature-Input: sig1=("@method" "@target-uri" "content-digest" "mcp-protocol-version" "mcp-session-id");created=1747641600;nonce="V8wA3pX2qF7nL1cR";keyid="7CECy79zsOm80SK3EgvHmsLM39PfUmbfSTxqOLhfgsM";tag="agent-instance-9f3e"
Signature: sig1=:LAH8BjcfcOcLojiuOBFWBo0hWDdMOGvAdEBM0h/MIRoNfj6Ae5OOKlEOUcKTOvXsyHkC5bV9D6m9QQDnR3QxDw==:

{"jsonrpc":"2.0","id":"req_42","method":"tools/call","params":{"name":"wallet_sign_swap","arguments":{}}}

Note that alg is absent (derived from the Ed25519 JWK), created and nonce are parameters (not components), and tag provides the stable client identity.

Rationale

Why RFC 9421 and not DPoP (SEP-1932)

DPoP (RFC 9449) and HTTP Message Signatures (RFC 9421) solve overlapping but distinct problems:

Concern DPoP RFC 9421 (this SEP)
Bind credential to a specific request Yes Yes
Requires OAuth flow Yes (sits inside one) No
Works for non-OAuth MCP servers (API key, anonymous) No Yes
Signs full request including body Optional, often not Yes (content-digest required)
Industry adoption beyond OAuth Low Higher (federation, payments, Web Bot Auth)
Layered above TLS termination Yes Yes

These should ship together: DPoP for the OAuth-bound case (where SEP-1932 already specifies it), RFC 9421 for the broader case where MCP servers want PoP without dragging in an OAuth dependency. Most wallet/payment use cases fall in the latter bucket.

Why fix SEP-1415 rather than replace it

The original 1415 proposal is fundamentally correct in mechanism — RFC 9421 is the right tool — but the discussion thread surfaced a series of small but blocking RFC-compliance bugs (incorrect signed-component lists, alg parameter usage, custom error envelopes, missing init-less story). This SEP keeps the mechanism and fixes each one explicitly, with citations to the underlying RFCs and the original thread comments.

Alternative: signed JSON-RPC envelopes

Considered and rejected. Signing the JSON-RPC payload alone misses HTTP-layer integrity (headers, URI, method), is incompatible with existing HTTP signing tooling and proxies, and forces every transport (stdio, WebSocket, Streamable HTTP) to re-invent a parallel scheme. RFC 9421 is HTTP-specific by design; stdio-only deployments are out of scope for this SEP and can be addressed in a follow-up if needed.

Alternative: mTLS

Considered and rejected as primary mechanism. mTLS is incompatible with the vast majority of MCP deployments that terminate TLS at a CDN or load balancer, and is impractical for local desktop clients. mTLS remains a valid additional layer where the deployment supports it.

Backward compatibility

Fully additive:

Security considerations

Threat coverage

Threat Without this SEP With this SEP
Bearer-token theft Full client compromise Bearer alone insufficient; signature still required
Request replay Full replay surface Bounded by 300s + nonce cache
Request tampering by proxy Undetectable Detected (content-digest covered)
Session-ID hijack Full takeover Hijacker lacks private key
Server-side credential exfiltration Token usable anywhere Only past signatures observable; key never exposed
TLS-terminating intermediary Sees bearer Sees signed requests it cannot forge

New attack surface

What this SEP does not address

Reference implementation

A reference implementation is planned in two parts:

  1. Server-side (Rust): an axum::middleware layer wrapping the rmcp Streamable-HTTP transport, verifying RFC 9421 signatures per §2 and emitting Accept-Signature per §3.3.
  2. Client-side (TypeScript): a fetch wrapper for the official @modelcontextprotocol/sdk that signs outbound requests using @waqas-shoukat/http-message-signatures or equivalent, with Ed25519 keys held in the browser's SubtleCrypto non-extractable form or a desktop OS keychain.

Both implementations will be linked from this SEP once Draft → In-Review transitions and a sponsor is assigned.

Open questions

  1. Should signatures/rotateKey be its own RPC method, or should rotation reuse initialize with cnf?
  2. Should the spec mandate Ed25519, or only RECOMMEND it (current text)?
  3. Should the freshness window (300s) be server-configurable and advertised in capabilities, or fixed by the spec?
  4. Is a future signed-response counterpart in scope for a separate SEP, or should we leave that to mTLS / TLS pinning indefinitely?

Extension governance (SEP-2133)

Field Value
Extension identifier io.modelcontextprotocol/http-message-signatures
Home repository modelcontextprotocol/ext-auth (on acceptance)
Incubation experimental-ext-http-message-signatures (proposed), associated with the MCP Authorization Working Group
Working Group MCP Authorization
License Apache-2.0
Stability Experimental during incubation; opt-in, disabled by default

Per SEP-2133 this is an Extensions Track proposal: reviewed by the Core Maintainers for inclusion as an official extension, and thereafter evolving independently under the ext-auth maintainers and the Authorization WG. It is OAuth-agnostic and complementary to DPoP (SEP-1932), sitting beside OAuth Client Credentials in ext-auth. A reference implementation in an official SDK is provided (see Reference implementation), satisfying the Extensions Track prerequisite. Breaking changes will use a new identifier (e.g. io.modelcontextprotocol/http-message-signatures-v2); non-breaking changes are negotiated through the settings object (§3.1).

References