<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Protocols on MdJawad</title><link>https://www.mdjawad.com/tags/protocols/</link><description>Recent content in Protocols on MdJawad</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 24 Aug 2026 13:16:33 +0000</lastBuildDate><atom:link href="https://www.mdjawad.com/tags/protocols/index.xml" rel="self" type="application/rss+xml"/><item><title>MCP Grew Up by Giving Things Away</title><link>https://www.mdjawad.com/posts/mcp-grew-up/</link><pubDate>Mon, 24 Aug 2026 09:00:00 +0800</pubDate><guid>https://www.mdjawad.com/posts/mcp-grew-up/</guid><description>Across five revisions the Model Context Protocol has steadily reduced the set of responsibilities it claims for itself, transferring authentication to the identity provider, client identity to the domain name system, consent to the administrator, and session state and routing to the application and the gateway. What the reduction leaves behind is a single trust boundary for which no better owner exists, and the attacks that have succeeded in practice are concentrated there.</description><content:encoded><![CDATA[<p><em>Covers MCP revisions 2024-11-05 through 2026-07-28. Verified against the specification in August 2026.</em></p>
<p>The Model Context Protocol has been revised five times since November 2024, and across those revisions the specification has steadily reduced the set of responsibilities it claims for itself. Authentication and token issuance were moved out first, over two revisions, to the identity provider an organisation already runs. Client identity was moved next, anchored to control of a domain rather than to a registration endpoint anyone could call. Consent followed in an extension that reached stable status in June 2026, relocating the decision from the employee to the administrator who is positioned to evaluate it, and session state and request routing were both moved out in the revision published in July, to the application and the gateway respectively.</p>
<p>The reduction accounts for most of the reason the protocol is now deployable inside an organisation, but the more useful observation concerns what it leaves behind. Each responsibility that was given up went to a party with a stronger claim to holding it, and after four rounds of this a single trust boundary remains for which no such party exists. That boundary is the point at which text authored by a third party enters a language model&rsquo;s context and is read through the same channel as the model&rsquo;s own instructions. Every attack class that has succeeded against MCP deployments in practice is located there, and the specification&rsquo;s security documentation, which enumerates eleven attack classes without addressing any of them, concedes as much by omission.</p>
<p>What follows establishes the architecture the argument depends on, works through the five revisions in order, and then examines the boundary that could not be reassigned. An <a href="https://www.mdjawad.com/posts/policy-engines-for-agents/">earlier post on policy engines</a> observed in passing that this specification had changed materially three times during 2025, which is the observation this piece was written to discharge.</p>
<h2 id="what-mcp-puts-where">What MCP puts where</h2>
<p>An MCP deployment has three components. A host application, which is a desktop client or an IDE or an internal agent, contains an MCP client, and that client speaks JSON-RPC to one or more MCP servers that expose capabilities. Stripped of the framing, this is an RPC protocol whose value proposition is combinatorial, since M applications integrating against N systems bespokely requires M×N pieces of integration code and a protocol reduces that to M+N. The property that makes integration cheap is that any client can talk to any server, and the security consequence follows directly, because any client can therefore talk to any server including one written by somebody hostile.</p>
<p>Servers expose tools, resources and prompts. Tools carry nearly all of the security weight, since a tool call is the moment at which a language model&rsquo;s output becomes an action taken in some other system. A tool is defined by a name, a JSON Schema describing its arguments, and a natural-language description, and it is worth being precise about where that description goes, because the whole of the second half of this piece depends on it. When a client asks a server what tools it offers, the descriptions in the reply are placed into the language model&rsquo;s context verbatim. They are read by the model in full and are ordinarily never displayed to the user.</p>
<p>Two transports are defined. The stdio transport launches a server as a subprocess of the host application, which means the server inherits the authority of the operating-system user, retrieves credentials from its environment, and presents no network surface. The HTTP transport, which since March 2025 has been Streamable HTTP, makes servers network-reachable and multi-tenant, and it is the transport for which the authorization specification is written. Authorization is optional at the protocol level and is defined only for HTTP, which is a detail worth carrying forward, because it determines how much of what follows applies to any given deployment.</p>
<h2 id="four-boundaries">Four boundaries</h2>
<p>A trust boundary is a location at which data or instructions pass from one authority domain into another, and where something is consequently obliged to validate what has arrived. The decomposition that matters for MCP is by authority domain rather than by network hop, for the reason that the boundary which turns out to be undefendable involves no network hop at all and would be invisible to any decomposition organised around transport.</p>
<p>Four such boundaries exist in a deployment of the shape described above. The first separates a user&rsquo;s intention from the action a model takes on their behalf, and it is crossed whenever an approval is granted or withheld. The second separates text supplied by a server from the model&rsquo;s reasoning, and it is crossed every time a tool description or a tool result is placed into context. The third separates the client from the server across a network, and it is crossed by every request that carries a credential. The fourth separates the server from whatever system it calls downstream, and it is crossed whenever delegated authority is spent.</p>
<p>Almost every MCP vulnerability disclosed to date is a failure to recognise one of the four. The figure below is worth returning to as the chronology proceeds, since each revision can be located against a specific boundary and the pattern that emerges from doing so is the argument of this piece.</p>

<div class="mcp-trust-boundaries" id="mcp-trust-boundaries-5c6c853c692d0a076f5b00c53b64272c">
  <style>
    .mcp-trust-boundaries{
      --bg2:var(--viz-bg); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .mcp-trust-boundaries *{box-sizing:border-box}
    .mcp-trust-boundaries .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .mcp-trust-boundaries .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .mcp-trust-boundaries .panel > *{position:relative}

    .mcp-trust-boundaries .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .mcp-trust-boundaries .sub{font-size:13.5px; color:var(--faint); margin-bottom:17px; line-height:1.5}

    .mcp-trust-boundaries .cols{display:grid; grid-template-columns:1fr 1.05fr; gap:16px; align-items:start}
    @media(max-width:820px){.mcp-trust-boundaries .cols{grid-template-columns:1fr}}
    .mcp-trust-boundaries .col-t{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--faint); margin-bottom:9px}

    .mcp-trust-boundaries .node{border:1px solid var(--line-strong); border-radius:10px; background:var(--panel2); padding:10px 13px; font-family:var(--viz-mono); font-size:12px; color:var(--ink-soft)}
    .mcp-trust-boundaries .node .nn{font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--faint); display:block; margin-bottom:3px}
    .mcp-trust-boundaries .host{border:1px dashed var(--line-strong); border-radius:12px; padding:11px; background:color-mix(in oklab, var(--violet) 4%, transparent)}
    .mcp-trust-boundaries .host > .hl{font-family:var(--viz-mono); font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:var(--faint); margin-bottom:9px}

    .mcp-trust-boundaries .bd{width:100%; text-align:left; display:grid; grid-template-columns:26px 1fr; gap:10px; align-items:center; background:transparent; border:none; border-left:2px dashed var(--line-strong); margin-left:13px; padding:11px 0 11px 14px; cursor:pointer; transition:.15s}
    .mcp-trust-boundaries .bd:hover .bl{color:var(--ink-soft)}
    .mcp-trust-boundaries .bd .num{font-family:var(--viz-mono); font-size:11px; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; border:1.5px solid var(--line-strong); color:var(--muted); background:var(--panel); transition:.15s}
    .mcp-trust-boundaries .bd .bl{font-family:var(--viz-mono); font-size:11.5px; color:var(--muted); line-height:1.4; transition:.15s}
    .mcp-trust-boundaries .bd .bl .st{display:block; font-size:9.5px; letter-spacing:.11em; text-transform:uppercase; margin-top:3px}

    .mcp-trust-boundaries .bd.s-yes .num{border-color:var(--k); color:var(--k)}
    .mcp-trust-boundaries .bd.s-yes .st{color:var(--k)}
    .mcp-trust-boundaries .bd.s-half .num{border-color:var(--q); color:var(--q)}
    .mcp-trust-boundaries .bd.s-half .st{color:var(--q)}
    .mcp-trust-boundaries .bd.s-no .num{border-color:var(--coral); color:var(--coral)}
    .mcp-trust-boundaries .bd.s-no .st{color:var(--coral)}
    .mcp-trust-boundaries .bd.on .bl{color:var(--ink)}
    .mcp-trust-boundaries .bd.on.s-yes{border-left-color:var(--k)} .mcp-trust-boundaries .bd.on.s-yes .num{background:var(--k); color:var(--viz-bg)}
    .mcp-trust-boundaries .bd.on.s-half{border-left-color:var(--q)} .mcp-trust-boundaries .bd.on.s-half .num{background:var(--q); color:var(--viz-bg)}
    .mcp-trust-boundaries .bd.on.s-no{border-left-color:var(--coral)} .mcp-trust-boundaries .bd.on.s-no .num{background:var(--coral); color:var(--viz-bg)}

    .mcp-trust-boundaries .det{border:1px solid var(--line-strong); border-radius:12px; background:var(--panel2); padding:15px 16px; transition:.25s}
    .mcp-trust-boundaries .det.s-yes{border-color:color-mix(in oklab, var(--k) 50%, transparent)}
    .mcp-trust-boundaries .det.s-half{border-color:color-mix(in oklab, var(--q) 50%, transparent)}
    .mcp-trust-boundaries .det.s-no{border-color:color-mix(in oklab, var(--coral) 55%, transparent); background:color-mix(in oklab, var(--coral) 5%, transparent)}
    .mcp-trust-boundaries .det .dh{font-family:var(--viz-mono); font-size:13px; color:var(--ink); margin-bottom:2px}
    .mcp-trust-boundaries .det .dw{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.09em; text-transform:uppercase; color:var(--faint); margin-bottom:12px}
    .mcp-trust-boundaries .det .row{padding:9px 0; border-top:1px solid var(--line)}
    .mcp-trust-boundaries .det .rk{font-family:var(--viz-mono); font-size:9.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--faint); margin-bottom:4px}
    .mcp-trust-boundaries .det .rv{font-size:13.5px; color:var(--ink-soft); line-height:1.55}
    .mcp-trust-boundaries .det .rv b{color:var(--ink); font-weight:600}
    .mcp-trust-boundaries .det .rv em{font-style:normal; color:var(--coral)}
    .mcp-trust-boundaries .det .chips{display:flex; flex-wrap:wrap; gap:6px; margin-top:2px}
    .mcp-trust-boundaries .det .chip{font-family:var(--viz-mono); font-size:10.5px; color:var(--muted); border:1px solid var(--line); border-radius:20px; padding:3px 9px; background:var(--panel)}
    .mcp-trust-boundaries .det.s-no .chip{border-color:color-mix(in oklab, var(--coral) 40%, transparent); color:color-mix(in oklab, var(--coral) 72%, var(--ink-soft))}

    .mcp-trust-boundaries .foot{margin-top:15px; padding-top:12px; border-top:1px solid var(--line); font-size:14px; color:var(--muted); line-height:1.6}
    .mcp-trust-boundaries .foot b{color:var(--ink-soft); font-weight:600}
    @media(max-width:640px){.mcp-trust-boundaries .panel{padding:14px} .mcp-trust-boundaries .bd{margin-left:4px}}
  </style>

  <div class="panel">
    <div class="panel-title">Four trust boundaries · two owners, one half, one none</div>
    <div class="sub">Where instructions or data cross between authority domains, validation is required. Select a boundary.</div>

    <div class="cols">
      <div>
        <div class="col-t">where the boundaries fall</div>

        <div class="node"><span class="nn">resource owner</span>user</div>
        <button class="bd s-half b-item" data-i="0">
          <span class="num">1</span>
          <span class="bl">intent → action<span class="st">half an owner</span></span>
        </button>

        <div class="host">
          <div class="hl">host application</div>
          <div class="node"><span class="nn">single token stream</span>model context</div>
          <button class="bd s-no b-item" data-i="1">
            <span class="num">2</span>
            <span class="bl">tool text → model context<span class="st">no owner</span></span>
          </button>
          <div class="node"><span class="nn">transport + auth</span>MCP client</div>
        </div>

        <button class="bd s-yes b-item" data-i="2">
          <span class="num">3</span>
          <span class="bl">network + identity<span class="st">handed off</span></span>
        </button>
        <div class="node"><span class="nn">resource server</span>MCP server</div>

        <button class="bd s-yes b-item" data-i="3">
          <span class="num">4</span>
          <span class="bl">delegated authority<span class="st">handed off</span></span>
        </button>
        <div class="node"><span class="nn">downstream systems</span>Jira · S3 · database · SaaS</div>
      </div>

      <div>
        <div class="col-t">what crosses, and who owns it</div>
        <div class="det o-det">
          <div class="dh o-dh"></div>
          <div class="dw o-dw"></div>
          <div class="row"><div class="rk">what crosses</div><div class="rv o-cross"></div></div>
          <div class="row"><div class="rk">primary risk</div><div class="rv o-risk"></div></div>
          <div class="row"><div class="rk">what the specification provides</div><div class="rv o-spec"></div></div>
          <div class="row"><div class="rk">handed to</div><div class="rv o-hand"></div></div>
          <div class="row"><div class="rk">attacks that live here</div><div class="chips o-chips"></div></div>
        </div>
      </div>
    </div>

    <div class="foot o-foot"></div>
  </div>

  <script>
  (function(){
    const root = document.getElementById('mcp-trust-boundaries-5c6c853c692d0a076f5b00c53b64272c');
    if(!root) return;

    const B = [
      {
        s:'half', h:'Boundary 1 · intent → action', w:'user ↔ host application',
        cross:'A human’s intention, becoming a call that spends money or moves data.',
        risk:'Blind approval and habituation. A user who has approved fifteen requests will approve the sixteenth without inspection.',
        spec:'Nothing normative. The tools spec says there <b>SHOULD</b> always be a human in the loop able to deny an invocation, and clients <b>SHOULD</b> show tool inputs before calling. Both are advisory, and both are about UI.',
        hand:'In part. Enterprise-Managed Authorization reassigned <b>connection</b> consent to the administrator, who is positioned to evaluate whether a server meets organisational requirements. Per-action intent was not reassigned.',
        chips:['consent fatigue','over-broad approval','fail-open elicitation']
      },
      {
        s:'no', h:'Boundary 2 · tool text → model context', w:'MCP client ↔ the model',
        cross:'Text written by whoever authored the server, delivered verbatim into the model’s reasoning. The user usually never sees it.',
        risk:'The model receives one undifferentiated stream, with no architectural separation between its instructions, the user’s input, and text returned by a tool.',
        spec:'One sentence, and it is a disclaimer: clients <b>MUST</b> consider tool annotations untrusted unless they come from trusted servers. <em>Zero of the eleven attack classes on the Security Best Practices page live here.</em>',
        hand:'No party. Every other boundary was reassigned to an entity with a stronger claim. No equivalent exists for determining whether a sentence is honest, because the party extending trust is a model and a model has no parser.',
        chips:['tool poisoning','line jumping','rug pulls','tool shadowing','injection via tool results']
      },
      {
        s:'yes', h:'Boundary 3 · network + identity', w:'MCP client ↔ MCP server',
        cross:'A request and a credential, across a network, to a server the client may have encountered moments earlier.',
        risk:'Token theft, audience confusion, and replay against a server for which the token was never issued.',
        spec:'The bulk of four revisions. Servers reclassified as pure Resource Servers, discovery via <b>RFC 9728</b>, mandatory audience binding via <b>RFC 8707</b>, issuer validation via <b>RFC 9207</b>, PKCE, CIMD, Origin validation.',
        hand:'The identity provider. MCP servers reverted to ordinary APIs whose only obligation is validating a token issued elsewhere.',
        chips:['confused deputy','mix-up','SSRF','DNS rebinding','token mis-redemption']
      },
      {
        s:'yes', h:'Boundary 4 · delegated authority', w:'MCP server ↔ downstream API',
        cross:'Authority the user delegated, being spent on their behalf against a system further down.',
        risk:'Over-privilege. A token honoured by several services converts a compromise of any one into a compromise of all.',
        spec:'Token passthrough named and forbidden: servers <b>MUST NOT</b> accept any tokens that were not explicitly issued for them, and <b>MUST NOT</b> accept or transit any other tokens.',
        hand:'The identity provider again, through <b>RFC 8693</b> token exchange, under which user context propagates and authority does not. Enforcement is located at the gateway.',
        chips:['token passthrough','audience confusion','blast-radius chaining']
      }
    ];

    const FOOT = 'Two boundaries were reassigned to a party with a stronger claim, and one was reassigned in part. <b>One has no available owner</b>, and the successful attacks are concentrated there.';

    const el = n => root.querySelector('.o-' + n);
    let cur = 2;   
                   

    function render(){
      const b = B[cur];
      root.querySelectorAll('.b-item').forEach(x => x.classList.toggle('on', +x.dataset.i === cur));
      el('det').className = 'det o-det s-' + b.s;
      el('dh').textContent = b.h;
      el('dw').textContent = b.w;
      el('cross').innerHTML = b.cross;
      el('risk').innerHTML = b.risk;
      el('spec').innerHTML = b.spec;
      el('hand').innerHTML = b.hand;
      el('chips').innerHTML = b.chips.map(c => '<span class="chip">' + c + '</span>').join('');
      el('foot').innerHTML = FOOT;
    }

    root.querySelectorAll('.b-item').forEach(x =>
      x.addEventListener('click', () => { cur = +x.dataset.i; render(); }));
    render();
  })();
  </script>
</div>

<h2 id="scope-and-audience">Scope and audience</h2>
<p>Bearer semantics are assumed here, and the consequence that matters is that a token presented by a party which acquired it improperly cannot be distinguished, at the point of validation, from the same token presented by the party it was issued to. Any constraint has therefore to be carried in the token itself. Scope has carried one such constraint since the first MCP revision to specify authorization at all, restricting the operations a holder may invoke. Audience carries the other, restricting the resource server at which the token will be accepted. Its absence from the March 2025 model is the specific defect the June revision was written to repair, since a token that enumerates scopes without naming a destination will be honoured by every resource server in the issuing domain that recognises those scopes, which turns the compromise of any single server into a credential-harvesting position against all of them.</p>
<p>The figure below carries one sequence per revision and is referred to from each of the five sections that follow. Reading the five in order shows the parties multiplying from two to three and the checks migrating outward from the server to the parties better placed to perform them.</p>

<div class="mcp-auth-sequence" id="mcp-auth-sequence-5c6c853c692d0a076f5b00c53b64272c">
  <style>
    .mcp-auth-sequence{
      --bg2:var(--viz-bg); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .mcp-auth-sequence *{box-sizing:border-box}
    .mcp-auth-sequence .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .mcp-auth-sequence .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .mcp-auth-sequence .panel > *{position:relative}

    .mcp-auth-sequence .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:9px}
    .mcp-auth-sequence .modes{display:grid; grid-template-columns:repeat(5,1fr); gap:5px; margin-bottom:12px}
    @media(max-width:700px){.mcp-auth-sequence .modes{grid-template-columns:repeat(3,1fr)}}
    .mcp-auth-sequence .md{font-family:var(--viz-mono); font-size:11px; color:var(--muted); background:var(--panel2); border:1px solid var(--line); border-radius:8px; padding:8px 6px; cursor:pointer; text-align:center; transition:.15s; line-height:1.35}
    .mcp-auth-sequence .md:hover{border-color:var(--line-strong); color:var(--ink-soft)}
    .mcp-auth-sequence .md b{display:block; font-weight:500}
    .mcp-auth-sequence .md span{display:block; font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:var(--faint); margin-top:3px}
    .mcp-auth-sequence .md.on{border-color:var(--k); background:color-mix(in oklab, var(--k) 11%, transparent); color:var(--ink)}
    .mcp-auth-sequence .md.on b{color:var(--k)} .mcp-auth-sequence .md.on span{color:var(--ink-soft)}
    .mcp-auth-sequence .sub{font-size:13.5px; color:var(--faint); margin-bottom:14px; line-height:1.5}

    .mcp-auth-sequence .parties{display:grid; gap:7px; margin-bottom:14px}
    .mcp-auth-sequence .parties.p2{grid-template-columns:1fr 1fr}
    .mcp-auth-sequence .parties.p3{grid-template-columns:1fr 1fr 1fr}
    @media(max-width:700px){.mcp-auth-sequence .parties.p2,.mcp-auth-sequence .parties.p3{grid-template-columns:1fr}}
    .mcp-auth-sequence .pty{border:1px solid var(--line-strong); border-radius:9px; background:var(--panel2); padding:9px 11px; font-family:var(--viz-mono); font-size:11.5px; color:var(--ink-soft); transition:.25s}
    .mcp-auth-sequence .pty span{display:block; font-size:9.5px; letter-spacing:.11em; text-transform:uppercase; color:var(--faint); margin-bottom:3px}
    .mcp-auth-sequence .pty.dual{border-color:var(--coral); background:color-mix(in oklab, var(--coral) 7%, transparent)}
    .mcp-auth-sequence .pty.dual span{color:var(--coral)}
    .mcp-auth-sequence .pty.hot{border-color:var(--q); background:color-mix(in oklab, var(--q) 9%, transparent)}

    .mcp-auth-sequence .cols{display:grid; grid-template-columns:1fr 1.2fr; gap:15px; align-items:start}
    @media(max-width:820px){.mcp-auth-sequence .cols{grid-template-columns:1fr}}
    .mcp-auth-sequence .col-t{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--faint); margin-bottom:9px}

    .mcp-auth-sequence .stp{width:100%; text-align:left; display:grid; grid-template-columns:22px 1fr; gap:9px; align-items:start; background:transparent; border:1px solid transparent; border-radius:9px; padding:7px 8px; cursor:pointer; transition:.15s}
    .mcp-auth-sequence .stp:hover{border-color:var(--line)}
    .mcp-auth-sequence .stp .n{font-family:var(--viz-mono); font-size:10px; width:20px; height:20px; border-radius:50%; display:flex; align-items:center; justify-content:center; border:1.5px solid var(--line-strong); color:var(--muted); background:var(--panel2)}
    .mcp-auth-sequence .stp .tx{font-family:var(--viz-mono); font-size:11.5px; color:var(--muted); line-height:1.45}
    .mcp-auth-sequence .stp .tx b{display:block; color:var(--ink-soft); font-weight:500}
    .mcp-auth-sequence .stp.on{border-color:var(--q); background:color-mix(in oklab, var(--q) 8%, transparent)}
    .mcp-auth-sequence .stp.on .n{background:var(--q); border-color:var(--q); color:var(--viz-bg)}
    .mcp-auth-sequence .stp.on .tx{color:var(--ink-soft)} .mcp-auth-sequence .stp.on .tx b{color:var(--q)}

    .mcp-auth-sequence .wire{border:1px solid var(--line-strong); border-radius:12px; background:var(--panel2); overflow:hidden}
    .mcp-auth-sequence .wire .wh{font-family:var(--viz-mono); font-size:10px; letter-spacing:.11em; text-transform:uppercase; color:var(--faint); padding:9px 13px; border-bottom:1px solid var(--line)}
    .mcp-auth-sequence .wire .wh .hl{color:var(--q)}
    .mcp-auth-sequence .wire .wb{display:block; margin:0; padding:12px 13px; font-family:var(--viz-mono); font-size:11px; line-height:1.65; color:var(--ink-soft); white-space:pre-wrap; word-break:break-word}
    .mcp-auth-sequence .wire .wb .hl{color:var(--q)}
    .mcp-auth-sequence .wire .wb .dim{color:var(--faint)}
    .mcp-auth-sequence .wire .wb .bad{color:var(--coral)}
    .mcp-auth-sequence .wire .wn{font-size:13px; color:var(--muted); line-height:1.55; padding:11px 13px; border-top:1px solid var(--line)}
    .mcp-auth-sequence .wire .wn b{color:var(--ink-soft); font-weight:600}

    .mcp-auth-sequence .foot{margin-top:15px; border:1px solid var(--line-strong); border-radius:12px; padding:13px 15px; background:var(--panel2)}
    .mcp-auth-sequence .foot .rh{display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap}
    .mcp-auth-sequence .foot .rt{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.11em; text-transform:uppercase; color:var(--faint)}
    .mcp-auth-sequence .btn{font-family:var(--viz-mono); font-size:11.5px; letter-spacing:.05em; color:var(--ink-soft); background:var(--panel); border:1px solid var(--line-strong); border-radius:8px; padding:7px 13px; cursor:pointer; transition:.15s}
    .mcp-auth-sequence .btn:hover{border-color:var(--coral); color:var(--coral)}
    .mcp-auth-sequence .rout{margin-top:11px; font-size:13.5px; line-height:1.6; color:var(--muted)}
    .mcp-auth-sequence .rout b{font-weight:600}
    .mcp-auth-sequence .rout.pass b{color:var(--k)} .mcp-auth-sequence .rout.fail b{color:var(--coral)}
    .mcp-auth-sequence .foot .plain{font-size:13.5px; color:var(--muted); line-height:1.6}
    .mcp-auth-sequence .foot .plain b{color:var(--ink-soft); font-weight:600}
    .mcp-auth-sequence code{font-family:var(--viz-mono); font-size:.9em; margin:0; padding:1px 4px; background:color-mix(in oklab, var(--viz-ink) 7%, transparent); border-radius:3px; color:inherit}
    @media(max-width:640px){.mcp-auth-sequence .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">What passes between the parties</div>
    <div class="modes o-modes"></div>
    <div class="sub o-sub"></div>
    <div class="parties o-parties"></div>

    <div class="cols">
      <div>
        <div class="col-t">sequence</div>
        <div class="o-steps"></div>
      </div>
      <div>
        <div class="col-t">on the wire</div>
        <div class="wire">
          <div class="wh o-wh"></div>
          <div class="wb o-wire"></div>
          <div class="wn o-wn"></div>
        </div>
      </div>
    </div>

    <div class="foot o-foot"></div>
  </div>

  <script>
  (function(){
    const root = document.getElementById('mcp-auth-sequence-5c6c853c692d0a076f5b00c53b64272c');
    if(!root) return;

    const MODES = [
      {
        d:'2024-11-05', g:'no token',
        sub:'The server is a child process of the host application. It inherits the authority of the operating-system user and reads its credentials from the environment.',
        parties:[{n:'host application',s:'contains the MCP client'},{n:'server subprocess',s:'stdio · inherits the OS user'}],
        steps:[
          {t:'the host launches the server', b:'spawn subprocess', hot:1,
           wh:'process launch',
           w:'npx -y @acme/mcp-server\n\n<span class="dim">environment inherited by the child</span>\nACME_API_KEY=<span class="bad">sk_live_…</span>',
           n:'No token is issued and no authorization decision is taken. Credentials reach the server because they were already present in the environment of the process that launched it.'},
          {t:'list the tools', b:'tools/list', hot:0,
           wh:'client → server · stdin',
           w:'{"jsonrpc":"2.0","id":1,"method":"tools/list"}',
           n:'JSON-RPC over stdin and stdout. There is no transport-level identity to establish, because both ends run as the same operating-system user.'},
          {t:'definitions return', b:'← tool definitions', hot:1,
           wh:'server → client · stdout',
           w:'{"tools":[{\n  "name":"get_weather",\n  <span class="hl">"description":"Get current weather for a location…"</span>,\n  "inputSchema":{"type":"object", …}}]}',
           n:'The <b>description</b> field is placed into model context verbatim. Nothing in this revision governs what it may contain, which is the boundary that later revisions also leave untouched.'},
          {t:'call a tool', b:'tools/call', hot:0,
           wh:'client → server · stdin',
           w:'{"method":"tools/call","params":{\n  "name":"get_weather",\n  "arguments":{"location":"Singapore"}}}',
           n:'The call executes with the full authority of the user who launched the host. Installation was the only point at which a decision was made.'}
        ],
        foot:{type:'note', text:'Nothing is delegated, so nothing is validated. The trust model is the one governing any locally installed command-line tool, and it ceased to be adequate the moment servers became network-reachable and multi-tenant.'}
      },
      {
        d:'2025-03-26', g:'one host, both roles',
        sub:'The MCP server runs the login, mints the tokens and holds the resource. One host performs the two roles OAuth separates deliberately.',
        parties:[{n:'MCP client',s:'oauth client'},{n:'mcp.acme.example',s:'authorization server AND resource server',dual:true}],
        steps:[
          {t:'unauthenticated request', b:'POST /mcp', hot:1,
           wh:'client → mcp.acme.example',
           w:'POST /mcp HTTP/1.1\nHost: mcp.acme.example\n\n<span class="dim">401 Unauthorized</span>',
           n:'The server refuses. Everything the client needs next is published on this same host, because the server is also the authorization server.'},
          {t:'find the endpoints', b:'GET /.well-known/oauth-authorization-server', hot:1,
           wh:'client → mcp.acme.example',
           w:'{\n  "authorization_endpoint": "https://mcp.acme.example/authorize",\n  "token_endpoint": "https://mcp.acme.example/token",\n  "registration_endpoint": "https://mcp.acme.example/register"\n}',
           n:'Every server author is thereby made responsible for consent screens, token rotation and revocation, which is work Okta staffs with dedicated teams.'},
          {t:'register at runtime', b:'POST /register', hot:1,
           wh:'dynamic client registration · RFC 7591',
           w:'POST /register\n{ "client_name": "Some Client",\n  "redirect_uris": ["https://…/callback"] }\n\n<span class="dim">→</span> { "client_id": "<span class="bad">c_8f21a…</span>" }',
           n:'An open registration endpoint permits any party to mint a client identity, including one presented to users as an official integration.'},
          {t:'authorize, then swap the code', b:'GET /authorize → POST /token', hot:1,
           wh:'token response',
           w:'{\n  "access_token": "eyJhbGciOi…",\n  "token_type": "Bearer"\n  <span class="bad">// no aud claim. valid anywhere.</span>\n}',
           n:'Nothing binds this token to the server for which it was issued. Tokens “should be validated”, and nothing requires that they name a destination.'},
          {t:'call the server', b:'POST /mcp + Bearer', hot:1,
           wh:'client → mcp.acme.example',
           w:'POST /mcp HTTP/1.1\nAuthorization: Bearer eyJhbGciOi…\n\n<span class="dim">200 OK</span>',
           n:'The call succeeds. The material question is what else this token will be accepted by.'}
        ],
        foot:{type:'replay', k:'fail',
          text:'Accepted. <b>mcp.other.example</b> receives a valid, unexpired, correctly signed token and honours it. A hostile server capturing this token holds a working credential for every other server in the ecosystem trusting the same issuer.'}
      },
      {
        d:'2025-06-18', g:'roles split',
        sub:'The MCP server reverts to an ordinary API. It validates tokens, and an authorization server whose job this is issues them.',
        parties:[{n:'MCP client',s:'oauth client'},{n:'mcp.acme.example',s:'resource server only'},{n:'auth.acme.example',s:'authorization server'}],
        steps:[
          {t:'unauthenticated request', b:'POST /mcp', hot:1,
           wh:'mcp.acme.example → client',
           w:'HTTP/1.1 401 Unauthorized\nWWW-Authenticate: Bearer\n  resource_metadata=<span class="hl">"https://mcp.acme.example/\n  .well-known/oauth-protected-resource"</span>',
           n:'The server is not requesting a credential. It is publishing a pointer to its own governance, which converts an unknown URL into a discoverable authorization graph.'},
          {t:'ask the resource who governs it', b:'GET /.well-known/oauth-protected-resource', hot:1,
           wh:'protected resource metadata · RFC 9728',
           w:'{\n  "resource": <span class="hl">"https://mcp.acme.example"</span>,\n  "authorization_servers": ["https://auth.acme.example"],\n  "scopes_supported": ["mcp:tools", "mcp:resources"]\n}',
           n:'<b>resource</b> is the canonical identifier that will appear as the token’s audience. Everything downstream anchors on this string.'},
          {t:'ask the authority for its endpoints', b:'GET /.well-known/oauth-authorization-server', hot:2,
           wh:'authorization server metadata · RFC 8414',
           w:'{\n  "issuer": "https://auth.acme.example",\n  "authorization_endpoint": "https://auth.acme.example/authorize",\n  "token_endpoint": "https://auth.acme.example/token",\n  "code_challenge_methods_supported": ["S256"]\n}',
           n:'The <b>issuer</b> is recorded at this point. Step 4 compares the authorization response against it, which is what defeats a mix-up attack.'},
          {t:'authorize, naming the resource', b:'GET /authorize?…&resource=…', hot:2,
           wh:'authorization request',
           w:'GET /authorize?response_type=code\n  &client_id=…&code_challenge=…&code_challenge_method=S256\n  &<span class="hl">resource=https%3A%2F%2Fmcp.acme.example</span>\n\n<span class="dim">→ ?code=…&iss=https://auth.acme.example</span>',
           n:'The <b>resource</b> parameter is the substance of the fix. Clients are required to send it whether or not the authorization server is known to support it.'},
          {t:'swap the code for a bound token', b:'POST /token', hot:2,
           wh:'token response',
           w:'{ "access_token": "eyJhbGciOi…", "token_type": "Bearer" }\n\n<span class="dim">// decoded claims</span>\n{ "iss": "https://auth.acme.example",\n  <span class="hl">"aud": "https://mcp.acme.example"</span>,\n  "scope": "mcp:tools" }',
           n:'Scope determines what the bearer may do and <b>aud</b> determines where. Both are required, and the second took three revisions to become mandatory.'},
          {t:'call the server', b:'POST /mcp + Bearer', hot:1,
           wh:'mcp.acme.example validates',
           w:'POST /mcp HTTP/1.1\nAuthorization: Bearer eyJhbGciOi…\n\n<span class="dim">// signature, expiry, and aud against\n// its own canonical identifier</span>\n<span class="dim">200 OK</span>',
           n:'The server’s entire security obligation is now four checks performed locally against a published key.'}
        ],
        foot:{type:'replay', k:'pass',
          text:'Rejected. <b>mcp.other.example</b> reads <code>aud</code>, finds a name other than its own, and returns 401. A systemic credential-theft risk becomes a contained one.'}
      },
      {
        d:'2025-11-25', g:'identity by retrieval',
        sub:'Discovery is unchanged. What changes is the identity the client presents once it reaches the authorization server, and how that identity is checked.',
        parties:[{n:'MCP client',s:'publishes its own metadata'},{n:'mcp.acme.example',s:'resource server'},{n:'auth.acme.example',s:'authorization server'}],
        steps:[
          {t:'discovery, as before', b:'401 → PRM → AS metadata', hot:1,
           wh:'client → resource server → authorization server',
           w:'HTTP/1.1 401 Unauthorized\nWWW-Authenticate: Bearer resource_metadata="…"\n\n<span class="dim">// identical to 2025-06-18 up to this point</span>',
           n:'Nothing in the discovery chain changed in this revision. The difference begins at the moment the client has to say who it is.'},
          {t:'present a URL as the client_id', b:'GET /authorize?client_id=https://…', hot:2,
           wh:'client → authorization server',
           w:'GET /authorize?response_type=code\n  &client_id=<span class="hl">https://client.example/.well-known/oauth-client</span>\n  &redirect_uri=https://client.example/cb\n  &resource=https%3A%2F%2Fmcp.acme.example',
           n:'The <b>client_id</b> is an HTTPS URL rather than an opaque string handed out by a registration endpoint.'},
          {t:'the authority fetches that URL', b:'AS → client.example', hot:2,
           wh:'authorization server → client.example  <span class="hl">· runs backwards</span>',
           w:'GET /.well-known/oauth-client\n\n<span class="dim">→</span> {\n  "client_id": "https://client.example/.well-known/oauth-client",\n  "client_name": "Example Client",\n  "redirect_uris": [<span class="hl">"https://client.example/cb"</span>]\n}',
           n:'This request travels backwards along the chain. Identity is resolved by <b>retrieval from a domain the client must control</b>, rather than by lookup in a table anyone could write to.'},
          {t:'validate, then proceed', b:'checks at the authority', hot:2,
           wh:'authorization server',
           w:'<span class="dim">// checks performed before consent</span>\nclient_id host   == metadata document host\nredirect_uri     ∈ metadata.redirect_uris\nTLS certificate  valid for client.example\n\n<span class="dim">→ 302 to redirect_uri with code + iss</span>',
           n:'What the identifier proves is possession of the DNS record and a valid certificate. Registration in the earlier sense has disappeared.'},
          {t:'token, unchanged', b:'POST /token', hot:2,
           wh:'token response',
           w:'{ <span class="hl">"aud": "https://mcp.acme.example"</span>,\n  "scope": "mcp:tools" }',
           n:'From here the flow is identical to 2025-06-18. Only the establishment of client identity was altered.'}
        ],
        foot:{type:'note', text:'The registration endpoint was the only workable answer to a many-to-many topology between clients and servers, so removing it outright was never available. Changing what an identifier <b>is</b> achieved the same end without removing anything.'}
      },
      {
        d:'2026-07-28', g:'stateless, header-routed',
        sub:'No handshake and no session. Each request carries what a server needs to interpret it, and a gateway can route on headers without reading the body.',
        parties:[{n:'MCP client',s:'oauth client'},{n:'gateway',s:'routes and meters on headers'},{n:'instances 1..N',s:'no shared session store'}],
        steps:[
          {t:'a self-describing request', b:'POST /mcp', hot:0,
           wh:'client → gateway',
           w:'POST /mcp HTTP/1.1\nAuthorization: Bearer eyJ…\n<span class="hl">Mcp-Method: tools/call</span>\n<span class="hl">Mcp-Name: create_basket</span>\n\n{"method":"tools/call","params":{…},\n "_meta":{\n   "io.modelcontextprotocol/protocolVersion":"2026-07-28",\n   "io.modelcontextprotocol/clientInfo":{…},\n   "io.modelcontextprotocol/clientCapabilities":{…}}}',
           n:'Protocol version, client identity and capabilities travel on every request. No handshake precedes it and there is no session for it to belong to.'},
          {t:'the gateway routes without parsing', b:'header-based routing', hot:1,
           wh:'gateway',
           w:'route   Mcp-Method=tools/call  → pool "write"\nmeter   Mcp-Name=create_basket\npolicy  scope check on the bearer token\n<span class="dim">// JSON body never deserialised</span>\n\n<span class="dim">→ instance 2</span>',
           n:'Method and tool name are readable as headers, so routing, metering and policy run without the gateway deserialising a body.'},
          {t:'a handle comes back', b:'← tool result', hot:2,
           wh:'instance 2 → client',
           w:'{"result":{"resultType":"complete",\n  "structuredContent":{"basket_id":"<span class="hl">bsk_a1b2c3</span>"}}}',
           n:'State that has to outlive the request is returned as an explicit handle. The protocol has no concept of one; from the wire it is an ordinary string in a tool result.'},
          {t:'the next call lands elsewhere', b:'tools/call + handle', hot:2,
           wh:'client → gateway → instance 5',
           w:'POST /mcp\n{"method":"tools/call","params":{\n  "name":"add_item",\n  "arguments":{"basket_id":"<span class="hl">bsk_a1b2c3</span>","sku":"…"}}}',
           n:'The handle travels as an ordinary tool argument, so this call can be served by an instance that never saw the first one.'},
          {t:'the instance verifies ownership', b:'bind handle to principal', hot:2,
           wh:'instance 5',
           w:'<span class="dim">// state keyed server-side</span>\nlookup  <span class="hl">sub + ":" + basket_id</span>\n        sub taken from the verified token,\n        never from the request body\n\n<span class="bad">reject when the handle belongs to another principal</span>',
           n:'Possession of a handle is not authentication. The specification names the corresponding risk as <b>state handle hijacking</b> and requires that handles be bound server-side to the authenticated user.'}
        ],
        foot:{type:'note', text:'Removing sessions removed a bearer credential carrying no signature, audience, expiry, scope or revocation path. The obligation it leaves behind is that an application-level handle must never be treated as proof of identity.'}
      }
    ];

    const st = { m:2, s:0 };
    const el = n => root.querySelector('.o-' + n);

    root.querySelector('.o-modes').innerHTML = MODES.map((m,i) =>
      '<button class="md m-item" data-i="' + i + '"><b>' + m.d + '</b><span>' + m.g + '</span></button>').join('');

    function render(){
      const M = MODES[st.m];
      if(st.s >= M.steps.length) st.s = M.steps.length - 1;
      const S = M.steps[st.s];

      root.querySelectorAll('.m-item').forEach(x => x.classList.toggle('on', +x.dataset.i === st.m));
      el('sub').textContent = M.sub;

      const pw = root.querySelector('.o-parties');
      pw.className = 'parties o-parties p' + M.parties.length;
      pw.innerHTML = M.parties.map((p,i) =>
        '<div class="pty' + (p.dual ? ' dual' : '') + (S.hot === i ? ' hot' : '') + '">'
        + '<span>' + p.s + '</span>' + p.n + '</div>').join('');

      el('steps').innerHTML = M.steps.map((x,i) =>
        '<button class="stp s-item' + (i === st.s ? ' on' : '') + '" data-i="' + i + '">'
        + '<span class="n">' + (i+1) + '</span>'
        + '<span class="tx"><b>' + x.t + '</b>' + x.b + '</span></button>').join('');
      root.querySelectorAll('.s-item').forEach(x =>
        x.addEventListener('click', () => { st.s = +x.dataset.i; render(); }));

      el('wh').innerHTML = S.wh;
      el('wire').innerHTML = S.w;
      el('wn').innerHTML = S.n;

      if(M.foot.type === 'replay'){
        el('foot').innerHTML =
          '<div class="rh"><div class="rt">the test · take the minted token to a different MCP server</div>'
          + '<button class="btn b-replay">replay at mcp.other.example</button></div>'
          + '<div class="rout o-rout">The token above was issued for <code>mcp.acme.example</code>. Press the button.</div>';
        root.querySelector('.b-replay').addEventListener('click', () => {
          const r = root.querySelector('.o-rout');
          r.className = 'rout o-rout ' + M.foot.k;
          r.innerHTML = M.foot.text;
        });
      } else {
        el('foot').innerHTML = '<div class="plain">' + M.foot.text + '</div>';
      }
    }

    root.querySelectorAll('.m-item').forEach(x =>
      x.addEventListener('click', () => { st.m = +x.dataset.i; st.s = 0; render(); }));
    render();
  })();
  </script>
</div>

<h2 id="2024-11-05-nothing-had-been-claimed-yet">2024-11-05: nothing had been claimed yet</h2>
<p><em>No boundary defended.</em></p>
<p>The first stable release specified the JSON-RPC core, the three primitives and the two transports, and it contained essentially no authorization guidance, extending to no direction on the use of Authorization headers. The omission was consistent with the deployment model of the period, in which servers ran as local subprocesses and inherited the authority of the operating-system user, so that the trust model governing an MCP server was the model governing any locally installed command-line tool. Installation was the only boundary carrying weight, and operating-system sandboxing was the appropriate control for it.</p>
<p>Setting the figure above to <code>2024-11-05</code> shows the whole of the exchange. The host spawns the server, the environment it inherits carries whatever credentials the server will use, and the two ends exchange <code>tools/list</code> and <code>tools/call</code> over stdin and stdout. No token appears anywhere in the sequence, because nothing has been delegated and there is consequently nothing to validate.</p>
<p>The relevant generalisation concerns the manner in which such models fail. A security model is rarely wrong at the time it is written, and becomes wrong when the deployment context shifts beneath it without anything in the specification announcing that the shift has occurred. MCP servers became network-reachable and multi-tenant during the first quarter of 2025, at which point the implicit model was silently invalidated.</p>
<h2 id="2025-03-26-the-specification-claims-too-much">2025-03-26: the specification claims too much</h2>
<p><em>Boundary 3, attempted.</em></p>
<p>The March revision replaced the original two-endpoint transport with Streamable HTTP, introduced tool annotations, and added JSON-RPC batching, which was removed three months afterwards. Its substantive change was an OAuth 2.1 framework for HTTP transports under which the MCP server was designated as its own authorization server in addition to its existing role as resource server.</p>
<p>The obligations carried by that designation are easy to understate when they are described abstractly. An authorization server conducts user authentication, records consent, issues and rotates and revokes tokens, validates PKCE parameters and manages client registration, which is a body of work that Okta and Microsoft staff with dedicated teams and still occasionally get wrong. Under the March design, exposing a single tool over HTTP required implementing that component first.</p>
<p>The <code>2025-03-26</code> sequence shows what that arrangement looks like on the wire. The 401 points at metadata published on the same host, registration happens at that host&rsquo;s own <code>/register</code> endpoint, and the authorization and token endpoints belong to it as well, so the exchange never leaves <code>mcp.acme.example</code>. The token returned at the end of it carries no <code>aud</code> claim.</p>
<p>The objection was raised structurally by Aaron Parecki, who edits the OAuth specifications at Okta, in <a href="https://aaronparecki.com/2025/04/03/15/oauth-for-model-context-protocol">Let&rsquo;s fix OAuth in MCP</a> on 3 April 2025. It did not rest on a defect in any particular implementation, which is what made it difficult to answer by patching one.</p>
<aside class="mcp-why">
  <style>
    .mcp-why{
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --accent:var(--viz-series-4);
      border:1px solid var(--line-strong);
      border-left:3px solid var(--accent);
      border-radius:10px;
      background:color-mix(in oklab, var(--viz-series-4) 5%, var(--viz-panel));
      padding:14px 17px; margin:1.75rem 0; color:var(--ink-soft);
    }
    .mcp-why > .wl{
      font-family:var(--viz-mono); font-size:10px; letter-spacing:.16em;
      text-transform:uppercase; color:var(--accent); margin-bottom:3px;
    }
    .mcp-why > .wt{
      font-size:15.5px; font-weight:600; color:var(--ink); line-height:1.45; margin-bottom:7px;
    }
    .mcp-why > .wb > :first-child{margin-top:0}
    .mcp-why > .wb > :last-child{margin-bottom:0}
    .mcp-why > .wb{font-size:15px; line-height:1.65}
    .mcp-why code{font-family:var(--viz-mono); font-size:.88em}
  </style>
  <div class="wl">why this decision</div>
  <div class="wt">Why the token issuer and the resource are kept apart</div>
  <div class="wb">OAuth separates the issuer of tokens from the resource those tokens open because combining the roles concentrates security-critical logic in software written by people whose primary concern lies elsewhere. Collapsing the separation yields one independent implementation of the hardest component in the stack for every server in the ecosystem.</div>
</aside>

<p>A second objection concerned enterprise architecture, where organisations operate exactly one authorization server, which is their identity provider, and treat every backend system as a resource. A design under which each of two hundred internal MCP servers is also an identity provider produces two hundred consent surfaces and no central point of revocation. Roughly three months elapsed between the publication of the criticism and the ratification of a redesigned specification, which is short for a standards process and indicates how much weight the problem was carrying.</p>
<h2 id="2025-06-18-identity-is-reassigned-to-the-identity-provider">2025-06-18: identity is reassigned to the identity provider</h2>
<p><em>Boundaries 3 and 4.</em></p>
<p>MCP servers were reclassified as pure OAuth resource servers, discovery was moved to Protected Resource Metadata under RFC 9728, and resource indicators under RFC 8707 were made mandatory. This is the pivotal revision, and the mechanism is most easily followed by switching the figure above to <code>2025-06-18</code> and stepping through it, since three parties now appear where there had been two.</p>
<p>A client issues a request carrying no token and receives a 401 whose header points at a metadata document, that document names both the authorization server governing the resource and the resource&rsquo;s own canonical identifier, and the client then presents itself to the named authorization server with a <code>resource</code> parameter identifying the server the token is intended for. The token returned carries a matching <code>aud</code> claim, which the MCP server validates against its own identifier before processing anything. The merit of the 401 is that it publishes a pointer to the resource&rsquo;s governance instead of requesting a credential, which converts an unknown URL into a discoverable authorization graph without client-side configuration. The header has been optional since 2025-11-25, with the same document reachable at the well-known path.</p>
<p>The failure this prevents is worth stating concretely. A server is stood up at <code>traveI.example.com</code>, where the fourth character is a capital i rather than a lowercase L and the substitution is invisible in most typefaces. A user induced to connect to it causes a genuine token to be issued by their own identity provider, and the impostor then presents that token at the real <code>travel.example.com</code>. The replay control in the figure demonstrates both designs. Once <code>resource</code> is mandatory the impostor&rsquo;s token names the impostor in its <code>aud</code> claim and is rejected on arrival, which reduces a systemic credential-theft risk to a contained one.</p>
<aside class="mcp-why">
  <style>
    .mcp-why{
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --accent:var(--viz-series-4);
      border:1px solid var(--line-strong);
      border-left:3px solid var(--accent);
      border-radius:10px;
      background:color-mix(in oklab, var(--viz-series-4) 5%, var(--viz-panel));
      padding:14px 17px; margin:1.75rem 0; color:var(--ink-soft);
    }
    .mcp-why > .wl{
      font-family:var(--viz-mono); font-size:10px; letter-spacing:.16em;
      text-transform:uppercase; color:var(--accent); margin-bottom:3px;
    }
    .mcp-why > .wt{
      font-size:15.5px; font-weight:600; color:var(--ink); line-height:1.45; margin-bottom:7px;
    }
    .mcp-why > .wb > :first-child{margin-top:0}
    .mcp-why > .wb > :last-child{margin-bottom:0}
    .mcp-why > .wb{font-size:15px; line-height:1.65}
    .mcp-why code{font-family:var(--viz-mono); font-size:.88em}
  </style>
  <div class="wl">why this decision</div>
  <div class="wt">Why MCP is stricter than the RFC it cites</div>
  <div class="wb">RFC 8707 makes <code>resource</code> optional, whereas MCP requires it and directs clients to <em>&ldquo;send this parameter regardless of whether authorization servers support it.&rdquo;</em> The interoperability cost is real, since nothing in discovery advertises whether a given authorization server honours the parameter and Keycloak requires a workaround. A property that is enforced only where both parties happen to support it is not a property anyone can build on.</div>
</aside>

<p>Token passthrough was named and prohibited in the same revision, with servers instructed that they <em>&ldquo;MUST NOT accept any tokens that were not explicitly issued for the MCP server.&rdquo;</em> Where a downstream call is required the sanctioned mechanism is token exchange under RFC 8693, in which an audience-bound token is presented to the authorization server and a different token, scoped to the downstream API, is returned, so that user context is propagated while authority is not.</p>
<h2 id="2025-11-25-client-identity-is-reassigned-to-domain-control">2025-11-25: client identity is reassigned to domain control</h2>
<p><em>Boundary 3.</em></p>
<p>Under dynamic client registration any party may register, with the consequence that a <code>client_id</code> carries no evidence about the identity of the caller and nothing prevents an attacker from registering a client that is presented to users as an official integration. The registration endpoint was nonetheless the only practical answer to the many-to-many topology between clients and servers, so removing it outright was never available and the problem had to be solved by changing what an identifier is.</p>
<p>Client ID Metadata Documents make the <code>client_id</code> an HTTPS URL that resolves to a document the client publishes, which the authorization server retrieves and caches. Registration in the earlier sense disappears, and what replaces it is a claim the authorization server is able to verify for itself at the moment the claim is made.</p>
<p>The <code>2025-11-25</code> sequence differs from its predecessor at exactly one point. When the client presents its URL-form <code>client_id</code>, the authorization server issues a request in the opposite direction, retrieving the metadata document from the client&rsquo;s own domain and validating the supplied <code>redirect_uri</code> against what it finds there. Every other step is unchanged.</p>
<aside class="mcp-why">
  <style>
    .mcp-why{
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --accent:var(--viz-series-4);
      border:1px solid var(--line-strong);
      border-left:3px solid var(--accent);
      border-radius:10px;
      background:color-mix(in oklab, var(--viz-series-4) 5%, var(--viz-panel));
      padding:14px 17px; margin:1.75rem 0; color:var(--ink-soft);
    }
    .mcp-why > .wl{
      font-family:var(--viz-mono); font-size:10px; letter-spacing:.16em;
      text-transform:uppercase; color:var(--accent); margin-bottom:3px;
    }
    .mcp-why > .wt{
      font-size:15.5px; font-weight:600; color:var(--ink); line-height:1.45; margin-bottom:7px;
    }
    .mcp-why > .wb > :first-child{margin-top:0}
    .mcp-why > .wb > :last-child{margin-bottom:0}
    .mcp-why > .wb{font-size:15px; line-height:1.65}
    .mcp-why code{font-family:var(--viz-mono); font-size:.88em}
  </style>
  <div class="wl">why this decision</div>
  <div class="wt">Why a URL is a better client identifier than a random string</div>
  <div class="wb">A random <code>client_id</code> is opaque and bears no verifiable relationship to anything. A URL is backed by domain control, since publishing metadata at a given origin requires possession of the corresponding DNS records and a valid TLS certificate. No new cryptographic mechanism was introduced, and an existing trust root already operated by every party was reused instead, which is generally the sign of a sound standards decision.</div>
</aside>

<p>One further change in this revision anticipates the second half of the argument. URL-mode elicitation relocates credential entry and OAuth and payment flows to a trusted external URL so that the secret never enters model context at all. Since a language model cannot be made to handle secrets reliably, the design routes them around the model instead of attempting to constrain its behaviour, and architectural avoidance of that kind is preferable to behavioural control wherever it is available.</p>
<h2 id="june-2026-consent-is-reassigned-to-the-administrator">June 2026: consent is reassigned to the administrator</h2>
<p><em>Boundary 1, in part.</em></p>
<p>An employee connecting an agent to fifteen internal servers is presented with fifteen consent dialogs, and the habituation that results constitutes a security failure rather than an inconvenience, because a user conditioned to approve fifteen requests will approve the sixteenth without inspection. The dialogs also pose a question the employee has no basis on which to answer.</p>
<p>Enterprise-Managed Authorization reached stable status on 18 June 2026. It is an extension, versioned independently of the core and forming no part of any of the five revisions, a point most secondary coverage reports incorrectly. The flow proceeds in three steps, beginning when the employee authenticates to the client through corporate single sign-on, continuing when the client exchanges the resulting identity assertion at the identity provider for a short-lived grant known as an ID-JAG, and completing when that grant is presented to the MCP authorization server in return for an access token.</p>

<div class="mcp-ema-chain" id="mcp-ema-chain-5c6c853c692d0a076f5b00c53b64272c">
  <style>
    .mcp-ema-chain{
      --bg2:var(--viz-bg); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .mcp-ema-chain *{box-sizing:border-box}
    .mcp-ema-chain .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .mcp-ema-chain .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .mcp-ema-chain .panel > *{position:relative}

    .mcp-ema-chain .panel-head{display:flex; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:6px}
    .mcp-ema-chain .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted)}
    .mcp-ema-chain .toggle{display:flex; border:1px solid var(--line-strong); border-radius:9px; overflow:hidden}
    .mcp-ema-chain .toggle button{font-family:var(--viz-mono); font-size:11.5px; color:var(--muted); background:var(--panel2); border:none; padding:8px 13px; cursor:pointer; transition:.15s}
    .mcp-ema-chain .toggle button:hover{color:var(--ink)}
    .mcp-ema-chain .toggle button.active{color:var(--viz-bg); font-weight:600}
    .mcp-ema-chain .toggle button.active[data-g="1"]{background:var(--k)}
    .mcp-ema-chain .toggle button.active[data-g="0"]{background:var(--coral)}
    .mcp-ema-chain .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px; line-height:1.5}

    .mcp-ema-chain .chain{display:flex; flex-direction:column; gap:0}
    .mcp-ema-chain .stp{border:1px solid var(--line); border-radius:11px; background:var(--panel2); padding:12px 14px; transition:.3s}
    .mcp-ema-chain .stp.dead{opacity:.34}
    .mcp-ema-chain .stp.pol{border-color:var(--q); background:color-mix(in oklab, var(--q) 7%, transparent)}
    .mcp-ema-chain .stp.stop{border-color:var(--coral); background:color-mix(in oklab, var(--coral) 8%, transparent)}
    .mcp-ema-chain .stp .sh{display:flex; align-items:baseline; gap:9px; flex-wrap:wrap}
    .mcp-ema-chain .stp .sn{font-family:var(--viz-mono); font-size:9.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--faint)}
    .mcp-ema-chain .stp .st{font-family:var(--viz-mono); font-size:12.5px; color:var(--ink-soft)}
    .mcp-ema-chain .stp.pol .st{color:var(--q)} .mcp-ema-chain .stp.stop .st{color:var(--coral)}
    .mcp-ema-chain .stp .sd{font-size:13px; color:var(--muted); line-height:1.55; margin-top:6px}
    .mcp-ema-chain .stp .sd b{color:var(--ink-soft); font-weight:600}
    .mcp-ema-chain .stp .tok{font-family:var(--viz-mono); font-size:11px; color:var(--faint); background:var(--panel); border:1px solid var(--line); border-radius:7px; padding:7px 9px; margin-top:8px; line-height:1.6; white-space:pre-wrap; word-break:break-word}
    .mcp-ema-chain .stp .tok .hl{color:var(--violet)}
    .mcp-ema-chain .arrow{font-family:var(--viz-mono); font-size:11px; color:var(--faint); padding:5px 0 5px 18px; transition:.3s}
    .mcp-ema-chain .arrow.dead{opacity:.3}
    .mcp-ema-chain .arrow.cut{color:var(--coral)}

    .mcp-ema-chain .out{margin-top:15px; border:1px solid var(--line-strong); border-radius:12px; padding:13px 15px; background:var(--panel2); transition:.25s}
    .mcp-ema-chain .out.ok{border-color:color-mix(in oklab, var(--k) 55%, transparent); background:color-mix(in oklab, var(--k) 6%, transparent)}
    .mcp-ema-chain .out.no{border-color:color-mix(in oklab, var(--coral) 60%, transparent); background:color-mix(in oklab, var(--coral) 6%, transparent)}
    .mcp-ema-chain .out .ot{font-family:var(--viz-mono); font-size:12px; letter-spacing:.1em; text-transform:uppercase; margin-bottom:6px}
    .mcp-ema-chain .out.ok .ot{color:var(--k)} .mcp-ema-chain .out.no .ot{color:var(--coral)}
    .mcp-ema-chain .out .ow{font-size:14px; color:var(--ink-soft); line-height:1.6}
    .mcp-ema-chain .out .ow b{color:var(--ink); font-weight:600}

    .mcp-ema-chain .tally{display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:13px}
    @media(max-width:560px){.mcp-ema-chain .tally{grid-template-columns:1fr}}
    .mcp-ema-chain .tl{border:1px solid var(--line); border-radius:10px; background:var(--panel2); padding:11px 13px}
    .mcp-ema-chain .tl .k{font-family:var(--viz-mono); font-size:9.5px; letter-spacing:.12em; text-transform:uppercase; color:var(--faint)}
    .mcp-ema-chain .tl .v{font-size:23px; font-variant-numeric:tabular-nums; line-height:1.2; margin-top:4px}
    .mcp-ema-chain .tl .s{font-family:var(--viz-mono); font-size:10.5px; color:var(--faint); margin-top:4px; line-height:1.5}
    .mcp-ema-chain .tl.bad .v{color:var(--coral)} .mcp-ema-chain .tl.good .v{color:var(--k)}
    .mcp-ema-chain .note{font-size:13.5px; color:var(--muted); font-style:italic; line-height:1.6; margin-top:14px; padding-top:12px; border-top:1px solid var(--line)}
    @media(max-width:640px){.mcp-ema-chain .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-head">
      <div class="panel-title">Enterprise-Managed Authorization · who decides</div>
      <div class="toggle">
        <button class="t-g active" data-g="1">in the approved group</button>
        <button class="t-g" data-g="0">not in the group</button>
      </div>
    </div>
    <div class="sub">An employee connects an agent to one of fifteen internal servers. The decision is taken at step two, without their involvement.</div>

    <div class="chain o-chain"></div>

    <div class="out o-out">
      <div class="ot o-ot"></div>
      <div class="ow o-ow"></div>
    </div>

    <div class="tally o-tally"></div>

    <div class="note">Scope note: this governs connection, not individual calls. Per-action authorization remains with the resource server.</div>
  </div>

  <script>
  (function(){
    const root = document.getElementById('mcp-ema-chain-5c6c853c692d0a076f5b00c53b64272c');
    if(!root) return;

    let allowed = true;
    const el = n => root.querySelector('.o-' + n);

    function steps(){
      return [
        { n:'step 1 · OIDC or SAML', t:'employee signs in to the client',
          d:'Corporate credentials, as used for every other work system. The client retains the identity assertion returned.',
          tok:'{ "iss": "https://idp.acme.example",\n  "sub": "<span class="hl">e.chen@acme.example</span>" }',
          live:true },
        { n:'step 2 · RFC 8693 token exchange', t:'client asks the IdP for an ID-JAG', pol:true,
          d:'The client presents the assertion and names the target server. <b>Policy is evaluated here by the employer</b>, against group membership, role and conditional access.',
          tok:'requested_token_type:\n  urn:ietf:params:oauth:token-type:<span class="hl">id-jag</span>\naudience: https://mcp.acme.example',
          live:true },
        allowed
          ? { n:'step 3 · RFC 7523 JWT bearer grant', t:'client swaps the ID-JAG for an access token',
              d:'The MCP authorization server validates the grant against the identity provider’s published keys and issues an audience-restricted access token.',
              tok:'{ "aud": "https://mcp.acme.example",\n  "sub": "<span class="hl">e.chen@acme.example</span>",\n  "scope": "mcp:tools" }',
              live:true }
          : { n:'step 3 · never reached', t:'no token is issued', stop:true,
              d:'The chain terminates at the identity provider. The client receives an error and the employee is presented with nothing to approve.',
              tok:'', live:false }
      ];
    }

    function render(){
      root.querySelectorAll('.t-g').forEach(b =>
        b.classList.toggle('active', (+b.dataset.g === 1) === allowed));

      const S = steps();
      el('chain').innerHTML = S.map((s,i) => {
        const cls = ['stp'];
        if(s.pol) cls.push('pol');
        if(s.stop) cls.push('stop');
        if(!s.live) cls.push('dead');
        const arrow = i === 0 ? '' :
          '<div class="arrow' + (S[i].live ? '' : ' cut') + '">' + (S[i].live ? '↓' : '↓  policy denied, chain ends') + '</div>';
        return arrow + '<div class="' + cls.join(' ') + '">'
          + '<div class="sh"><span class="sn">' + s.n + '</span><span class="st">' + s.t + '</span></div>'
          + '<div class="sd">' + s.d + '</div>'
          + (s.tok ? '<div class="tok">' + s.tok + '</div>' : '')
          + '</div>';
      }).join('');

      if(allowed){
        el('out').className = 'out o-out ok';
        el('ot').textContent = 'connected · zero prompts';
        el('ow').innerHTML = 'No consent screen was presented, because the decision was never the employee’s to take. Employees cannot assess whether a server satisfies organisational data-handling requirements, and requiring them to produces habituation without producing safety. The specification is explicit: <b>do not redirect the user to the MCP authorization server’s authorization endpoint</b>.';
      } else {
        el('out').className = 'out o-out no';
        el('ot').textContent = 'refused at the identity provider';
        el('ow').innerHTML = 'Nothing was issued and nothing was displayed, and the second condition is the significant one. Under ordinary OAuth this user would have reached a consent screen and been free to approve it, whereas here the refusal occurs <b>before any human is positioned to approve anything</b>.';
      }

      el('tally').innerHTML =
        '<div class="tl bad"><div class="k">per-server consent · ordinary OAuth</div><div class="v">15</div>'
        + '<div class="s">one prompt per server; by the fifteenth they are not being read.</div></div>'
        + '<div class="tl good"><div class="k">per-server consent · EMA</div><div class="v">0</div>'
        + '<div class="s">one policy, set once, revocable from a single console.</div></div>';
    }

    root.querySelectorAll('.t-g').forEach(b =>
      b.addEventListener('click', () => { allowed = +b.dataset.g === 1; render(); }));
    render();
  })();
  </script>
</div>

<p>Policy is evaluated at the middle step by the employer, and the denied branch in the figure is the more instructive of the two. Where the user falls outside the approved group the chain terminates at the identity provider, no token is issued, and nothing whatever is displayed to the employee, whereas under ordinary OAuth that same user would have been shown a consent dialog and permitted to approve it.</p>
<aside class="mcp-why">
  <style>
    .mcp-why{
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --accent:var(--viz-series-4);
      border:1px solid var(--line-strong);
      border-left:3px solid var(--accent);
      border-radius:10px;
      background:color-mix(in oklab, var(--viz-series-4) 5%, var(--viz-panel));
      padding:14px 17px; margin:1.75rem 0; color:var(--ink-soft);
    }
    .mcp-why > .wl{
      font-family:var(--viz-mono); font-size:10px; letter-spacing:.16em;
      text-transform:uppercase; color:var(--accent); margin-bottom:3px;
    }
    .mcp-why > .wt{
      font-size:15.5px; font-weight:600; color:var(--ink); line-height:1.45; margin-bottom:7px;
    }
    .mcp-why > .wb > :first-child{margin-top:0}
    .mcp-why > .wb > :last-child{margin-bottom:0}
    .mcp-why > .wb{font-size:15px; line-height:1.65}
    .mcp-why code{font-family:var(--viz-mono); font-size:.88em}
  </style>
  <div class="wl">why this decision</div>
  <div class="wt">Why the consent decision moved to the administrator</div>
  <div class="wb">Consent exists to place a decision with the party able to evaluate the risk. For an individual connecting a personal calendar that party is the user. For an employee connecting to a corporate system it never was, since employees cannot assess whether a server satisfies organisational data-handling requirements, and requiring them to attempt it produces habituation without producing safety.</div>
</aside>

<p>Two qualifications apply to the extension. It determines whether a user may connect a given client to a given server and with which scopes, and it does not address whether a particular call carrying particular arguments should proceed, which remains a separate question at a separate boundary. It also landed outside the core, as the tasks feature did in the same period, which indicates that the core has ceased to be the location at which new capability is added.</p>
<h2 id="2026-07-28-state-and-routing-are-reassigned">2026-07-28: state and routing are reassigned</h2>
<p><em>Boundary 3.</em></p>
<p>The July revision is the most substantial since remote MCP was introduced, and its central change was the removal of protocol sessions. The <code>initialize</code> handshake and the <code>Mcp-Session-Id</code> header were both retired, and each request now describes itself by carrying its protocol version and client identity and capabilities in a metadata field, so that any request may be handled by any instance sitting behind a conventional load balancer.</p>
<p>Two considerations favoured the change independently of one another. Operationally, a session identifier implies server affinity, and affinity implies sticky routing or replication or a shared store, each of which imposes a cost on every operator running the protocol at scale. The security argument is the stronger of the two and follows from the treatment of audience above, since a session identifier whose possession grants continued access functions as a bearer credential while lacking every protection an access token carries, having no signature and no audience restriction and no expiry and no scope and no revocation path. The specification was accordingly obliged to require that session identifiers be globally unique, cryptographically secure, bound to user identity, and never treated as authentication in themselves.</p>
<aside class="mcp-why">
  <style>
    .mcp-why{
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --accent:var(--viz-series-4);
      border:1px solid var(--line-strong);
      border-left:3px solid var(--accent);
      border-radius:10px;
      background:color-mix(in oklab, var(--viz-series-4) 5%, var(--viz-panel));
      padding:14px 17px; margin:1.75rem 0; color:var(--ink-soft);
    }
    .mcp-why > .wl{
      font-family:var(--viz-mono); font-size:10px; letter-spacing:.16em;
      text-transform:uppercase; color:var(--accent); margin-bottom:3px;
    }
    .mcp-why > .wt{
      font-size:15.5px; font-weight:600; color:var(--ink); line-height:1.45; margin-bottom:7px;
    }
    .mcp-why > .wb > :first-child{margin-top:0}
    .mcp-why > .wb > :last-child{margin-bottom:0}
    .mcp-why > .wb{font-size:15px; line-height:1.65}
    .mcp-why code{font-family:var(--viz-mono); font-size:.88em}
  </style>
  <div class="wl">why this decision</div>
  <div class="wt">Why the session was deleted rather than hardened</div>
  <div class="wb">Requirements of that shape are what gets written when a mechanism is inherently hazardous, and they support a generalisation worth applying well beyond MCP. Where a security requirement takes the form of a list of errors implementers must remember to avoid, the feature should be considered for removal instead, since a substantial proportion of implementers will fail at least one item on any such list. Deleting the mechanism deletes the whole family of mistakes attached to it.</div>
</aside>

<p>Any accompanying claim that state was eliminated would be overstated. Persistence remains necessary for ordinary functionality, and servers now issue an explicit handle which is passed back as a conventional tool argument, with the consequence that the same document which removed sessions introduces state handle hijacking as a named attack and requires that handles be verified on arrival, never treated as authentication, generated non-deterministically, and bound server-side to the authenticated user. Those requirements correspond closely to the ones just deleted, and the correspondence is directly verifiable, since the superseded session-hijacking guidance remains published at the 2025-11-25 URL that the current security page links to. What improved is the placement, since the risk now sits in a layer holding enough context to manage it, a point the tools specification states economically in observing that <em>&ldquo;a handle is a name, not a capability.&rdquo;</em></p>
<p>Routing was reassigned in the same revision. The <code>Mcp-Method</code> and <code>Mcp-Name</code> headers permit a gateway to route and meter without parsing JSON bodies, and <code>x-mcp-header</code> goes further by allowing a tool&rsquo;s input schema to designate a parameter for mirroring into an HTTP header, so that intermediaries can route on argument values without reading the payload. Tool definitions are now being shaped around what the intermediary needs to see.</p>
<p>The <code>2026-07-28</code> sequence shows the three consequences together. A request carries its protocol version and client identity in <code>_meta</code> and its method and tool name in headers, the gateway routes and meters on those headers without deserialising the body, and a state handle returned by one instance is presented to a different instance on the following call, which verifies that the handle belongs to the principal named in the token.</p>

<div class="mcp-revision-timeline" id="mcp-revision-timeline-5c6c853c692d0a076f5b00c53b64272c">
  <style>
    .mcp-revision-timeline{
      --bg2:var(--viz-bg); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .mcp-revision-timeline *{box-sizing:border-box}
    .mcp-revision-timeline .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .mcp-revision-timeline .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .mcp-revision-timeline .panel > *{position:relative}
    .mcp-revision-timeline .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted); margin-bottom:5px}
    .mcp-revision-timeline .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px; line-height:1.5}

    .mcp-revision-timeline .rail{display:grid; grid-template-columns:repeat(6,1fr); gap:5px; margin-bottom:17px}
    @media(max-width:760px){.mcp-revision-timeline .rail{grid-template-columns:repeat(3,1fr)}}
    .mcp-revision-timeline .rv{background:var(--panel2); border:1px solid var(--line); border-radius:9px; padding:8px 7px; cursor:pointer; text-align:left; transition:.15s}
    .mcp-revision-timeline .rv:hover{border-color:var(--line-strong)}
    .mcp-revision-timeline .rv .d{font-family:var(--viz-mono); font-size:10.5px; color:var(--ink-soft); letter-spacing:.02em; display:block}
    .mcp-revision-timeline .rv .g{font-family:var(--viz-mono); font-size:9.5px; letter-spacing:.09em; text-transform:uppercase; color:var(--faint); display:block; margin-top:4px; line-height:1.35}
    .mcp-revision-timeline .rv.ext{border-style:dashed}
    .mcp-revision-timeline .rv.on{border-color:var(--k); background:color-mix(in oklab, var(--k) 10%, transparent)}
    .mcp-revision-timeline .rv.on .d{color:var(--k)} .mcp-revision-timeline .rv.on .g{color:var(--ink-soft)}

    .mcp-revision-timeline .cols{display:grid; grid-template-columns:1fr 1.08fr; gap:15px; align-items:start}
    @media(max-width:820px){.mcp-revision-timeline .cols{grid-template-columns:1fr}}
    .mcp-revision-timeline .col-t{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--faint); margin-bottom:9px}

    .mcp-revision-timeline .holds{border:1px solid var(--line-strong); border-radius:12px; background:var(--panel2); padding:11px 13px}
    .mcp-revision-timeline .h{padding:7px 0; border-bottom:1px solid var(--line); font-family:var(--viz-mono); font-size:11.5px; color:var(--ink-soft); line-height:1.45; transition:.3s}
    .mcp-revision-timeline .h:last-child{border-bottom:none}
    .mcp-revision-timeline .h .own{display:block; font-size:10px; letter-spacing:.06em; color:var(--faint); margin-top:3px}
    .mcp-revision-timeline .h.gone{color:var(--faint); text-decoration:line-through; text-decoration-color:color-mix(in oklab, var(--k) 70%, transparent)}
    .mcp-revision-timeline .h.gone .own{text-decoration:none; color:var(--k)}
    .mcp-revision-timeline .h.just{background:color-mix(in oklab, var(--k) 9%, transparent); margin:0 -13px; padding-left:13px; padding-right:13px}
    .mcp-revision-timeline .h.never{color:var(--coral)}
    .mcp-revision-timeline .h.never .own{color:color-mix(in oklab, var(--coral) 68%, var(--muted))}
    .mcp-revision-timeline .count{font-family:var(--viz-mono); font-size:10.5px; color:var(--faint); margin-top:10px; letter-spacing:.05em}
    .mcp-revision-timeline .count b{color:var(--k)}

    .mcp-revision-timeline .det{border:1px solid var(--line-strong); border-radius:12px; background:var(--panel2); padding:14px 15px}
    .mcp-revision-timeline .det .q{font-size:14.5px; color:var(--ink); line-height:1.5; margin-bottom:3px}
    .mcp-revision-timeline .det .am{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--faint); margin-bottom:12px}
    .mcp-revision-timeline .det .row{padding:9px 0; border-top:1px solid var(--line)}
    .mcp-revision-timeline .det .rk{font-family:var(--viz-mono); font-size:9.5px; letter-spacing:.13em; text-transform:uppercase; margin-bottom:5px}
    .mcp-revision-timeline .det .rk.add{color:var(--k)} .mcp-revision-timeline .det .rk.rem{color:var(--coral)}
    .mcp-revision-timeline .det ul{margin:0; padding-left:15px}
    .mcp-revision-timeline .det li{font-size:13px; color:var(--ink-soft); line-height:1.55; margin-bottom:2px}
    .mcp-revision-timeline .det li code{font-family:var(--viz-mono); font-size:11.5px; color:var(--violet)}
    .mcp-revision-timeline .det .none{font-size:13px; color:var(--faint); font-style:italic}
    @media(max-width:640px){.mcp-revision-timeline .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-title">Five revisions, one extension · tracked by subtraction</div>
    <div class="sub">The left column is what the trusted core still holds. Watch it shrink, and watch the one item that never finds an owner.</div>

    <div class="rail o-rail"></div>

    <div class="cols">
      <div>
        <div class="col-t">what the protocol holds</div>
        <div class="holds o-holds"></div>
        <div class="count o-count"></div>
      </div>
      <div>
        <div class="col-t">the revision</div>
        <div class="det">
          <div class="q o-q"></div>
          <div class="am o-am"></div>
          <div class="row"><div class="rk add">added</div><div class="o-add"></div></div>
          <div class="row"><div class="rk rem">removed or deprecated</div><div class="o-rem"></div></div>
        </div>
      </div>
    </div>
  </div>

  <script>
  (function(){
    const root = document.getElementById('mcp-revision-timeline-5c6c853c692d0a076f5b00c53b64272c');
    if(!root) return;

    
    const HOLDS = [
      { n:'transport and message framing',        gaveAt:99, own:'' },
      { n:'tools, resources, prompts',            gaveAt:99, own:'' },
      { n:'authentication and token minting',     gaveAt:2,  own:'→ the identity provider' },
      { n:'client identity and registration',     gaveAt:3,  own:'→ domain control, via DNS and TLS' },
      { n:'user consent',                         gaveAt:4,  own:'→ the administrator, for organisations' },
      { n:'session state',                        gaveAt:5,  own:'→ the application, as explicit handles' },
      { n:'request routing and metering',         gaveAt:5,  own:'→ the gateway, via headers' },
      { n:'whether a tool description is honest', gaveAt:-1, own:'nobody has a better claim' }
    ];

    const R = [
      { d:'2024-11-05', g:'the local era',
        q:'How does an AI application talk to a tool at all?',
        am:'authorization model: none specified',
        add:['JSON-RPC 2.0 core', 'Tools, resources and prompts', '<code>stdio</code> and HTTP+SSE transports'],
        rem:[] },
      { d:'2025-03-26', g:'claims too much',
        q:'How do we authenticate a remote server?',
        am:'authorization model: the server is its own authorization server',
        add:['OAuth 2.1 framework for HTTP transports', 'Streamable HTTP transport', 'Tool annotations (<code>readOnlyHint</code> and friends)', 'Dynamic Client Registration, PKCE', 'JSON-RPC batching'],
        rem:['HTTP+SSE transport deprecated here, not in 2026'] },
      { d:'2025-06-18', g:'identity out',
        q:'Who should be the authorization server?',
        am:'authorization model: the server is a pure OAuth Resource Server',
        add:['Protected Resource Metadata discovery (<code>RFC 9728</code>)', 'Mandatory resource indicators (<code>RFC 8707</code>)', 'Security Best Practices page', 'Elicitation, structured tool output, resource links', '<code>MCP-Protocol-Version</code> header required'],
        rem:['JSON-RPC batching, for want of a compelling use case', 'The server’s job as an authorization server'] },
      { d:'2025-11-25', g:'client identity out',
        q:'Which client is this, and can it prove it?',
        am:'authorization model: <code>client_id</code> becomes a URL you must control',
        add:['Client ID Metadata Documents (SEP-991)', 'OIDC Discovery, incremental scope consent', 'URL-mode elicitation, so secrets skip the model', 'Experimental tasks, icons, sampling with tools'],
        rem:['<code>includeContext: "allServers"</code> deprecated'] },
      { d:'2026-06-18', g:'consent out', ext:true,
        q:'Who decides, when the user works for someone?',
        am:'an extension, versioned separately from the core',
        add:['Enterprise-Managed Authorization goes stable', 'Identity assertion exchanged for an ID-JAG at the IdP', 'Policy evaluated by the employer, not the employee', 'Zero consent screens across every approved server'],
        rem:['The per-server consent prompt, inside an organisation'] },
      { d:'2026-07-28', g:'state and routing out',
        q:'How does this run at hyperscale without state as a liability?',
        am:'authorization model: hardened, and stateless throughout',
        add:['Stateless core; every request self-describing via <code>_meta</code>', 'Multi Round-Trip Requests replace held-open streams', '<code>Mcp-Method</code>, <code>Mcp-Name</code> and <code>x-mcp-header</code> routing', 'Cacheable list results; <code>RFC 9207</code> issuer validation', 'A formal deprecation policy with a 12-month floor'],
        rem:['The <code>initialize</code> handshake', 'The <code>Mcp-Session-Id</code> header, and protocol sessions', 'Dynamic Client Registration, in favour of CIMD', 'Roots, sampling and logging'] }
    ];

    let cur = 5;
    const el = n => root.querySelector('.o-' + n);

    root.querySelector('.o-rail').innerHTML = R.map((r,i) =>
      '<button class="rv r-item' + (r.ext ? ' ext' : '') + '" data-i="' + i + '">'
      + '<span class="d">' + r.d + '</span><span class="g">' + r.g + '</span></button>').join('');

    function render(){
      const r = R[cur];
      root.querySelectorAll('.r-item').forEach(x => x.classList.toggle('on', +x.dataset.i === cur));

      el('holds').innerHTML = HOLDS.map(h => {
        const gone = h.gaveAt >= 0 && h.gaveAt <= cur;
        const just = h.gaveAt === cur;
        const never = h.gaveAt === -1;
        const cls = ['h']; if(gone) cls.push('gone'); if(just) cls.push('just'); if(never) cls.push('never');
        const own = (gone || never) ? '<span class="own">' + h.own + '</span>' : '';
        return '<div class="' + cls.join(' ') + '">' + h.n + own + '</div>';
      }).join('');

      const held = HOLDS.filter(h => !(h.gaveAt >= 0 && h.gaveAt <= cur)).length;
      const given = HOLDS.length - held;
      el('count').innerHTML = 'holds <b>' + held + '</b> of ' + HOLDS.length
        + ' · handed off ' + given;

      el('q').textContent = r.q;
      el('am').innerHTML = r.am;
      el('add').innerHTML = '<ul>' + r.add.map(x => '<li>' + x + '</li>').join('') + '</ul>';
      el('rem').innerHTML = r.rem.length
        ? '<ul>' + r.rem.map(x => '<li>' + x + '</li>').join('') + '</ul>'
        : '<div class="none">Nothing yet. There was nothing to give away.</div>';
    }

    root.querySelectorAll('.r-item').forEach(x =>
      x.addEventListener('click', () => { cur = +x.dataset.i; render(); }));
    render();
  })();
  </script>
</div>

<p>Reading the boundary tags on the five sections above in sequence produces the pattern the rest of this piece depends on. Four revisions operate on boundary 3 or boundary 4, one addresses boundary 1 in part, and boundary 2 is touched by none of them.</p>
<h2 id="the-responsibility-that-could-not-be-reassigned">The responsibility that could not be reassigned</h2>
<p><em>Boundary 2.</em></p>
<p>A language model receives a single undifferentiated token stream. No architectural separation exists within it between instructions written by the developer, input supplied by the user, and text returned by a tool, and training can shift the probability that a model privileges one over another without establishing a boundary that holds under pressure. Tool descriptions authored by whoever wrote the server are delivered into that stream verbatim and are ordinarily displayed to nobody. The limit this places on any specification is precise, in that a specification can constrain what a server is permitted to do while remaining unable to constrain what text does to a model once the text has arrived.</p>
<p>The specification appears to accept as much. The attack classes enumerated on its Security Best Practices page are the confused deputy problem, token passthrough, server-side request forgery, state handle hijacking, local server compromise, OAuth authorization URL validation, stdio transport security in proxy scenarios, mix-up attacks, localhost redirect URI impersonation, CIMD trust policies and scope minimisation. Eleven classes, and each of them is situated at a boundary for which some party held a better claim. None of them addresses what a tool description does after it has reached the model.</p>
<p>One vulnerability is worth working through, because it shows why the control most clients reach for does not help. Trail of Bits described it in April 2025 under the name <a href="https://blog.trailofbits.com/2025/04/21/jumping-the-line-how-mcp-servers-can-attack-you-before-you-ever-use-them/">line jumping</a>. The reply to a tool listing enters model context before any tool has been invoked, and therefore before an approval gate has an event available to intercept.</p>

<div class="mcp-line-jumping" id="mcp-line-jumping-5c6c853c692d0a076f5b00c53b64272c">
  <style>
    .mcp-line-jumping{
      --bg2:var(--viz-bg); --panel:var(--viz-panel); --panel2:var(--viz-raised);
      --ink:var(--viz-ink); --ink-soft:var(--viz-ink-dim); --muted:var(--viz-ink-muted);
      --faint:color-mix(in oklab, var(--viz-ink-muted) 72%, var(--viz-panel));
      --line:var(--viz-border); --line-strong:var(--viz-border-strong);
      --q:var(--viz-series-4); --k:var(--viz-series-1);
      --coral:var(--viz-series-2); --violet:var(--viz-series-3);
      color:var(--ink); margin:2rem 0; max-width:100%;
    }
    .mcp-line-jumping *{box-sizing:border-box}
    .mcp-line-jumping .panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line-strong); border-radius:16px; padding:20px; box-shadow:0 24px 60px -36px color-mix(in oklab, var(--viz-ink) 45%, transparent); position:relative; overflow:hidden}
    .mcp-line-jumping .panel::before{content:""; position:absolute; inset:0; pointer-events:none; border-radius:16px; background:linear-gradient(90deg,var(--line) 1px,transparent 1px) 0 0/26px 26px,linear-gradient(180deg,var(--line) 1px,transparent 1px) 0 0/26px 26px; opacity:.30; -webkit-mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%); mask:radial-gradient(120% 120% at 50% 0%,#000,transparent 78%)}
    .mcp-line-jumping .panel > *{position:relative}

    .mcp-line-jumping .panel-head{display:flex; justify-content:space-between; align-items:flex-start; gap:14px; flex-wrap:wrap; margin-bottom:6px}
    .mcp-line-jumping .panel-title{font-family:var(--viz-mono); font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:var(--muted)}
    .mcp-line-jumping .sub{font-size:13.5px; color:var(--faint); margin-bottom:16px; line-height:1.5}
    .mcp-line-jumping .toggle{display:flex; gap:0; border:1px solid var(--line-strong); border-radius:9px; overflow:hidden; flex-shrink:0}
    .mcp-line-jumping .toggle button{font-family:var(--viz-mono); font-size:11.5px; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); background:var(--panel2); border:none; padding:8px 13px; cursor:pointer; transition:.15s}
    .mcp-line-jumping .toggle button:hover{color:var(--ink)}
    .mcp-line-jumping .toggle button.active{color:var(--viz-bg); background:var(--coral); font-weight:600}

    .mcp-line-jumping .gatebar{margin-bottom:15px}
    .mcp-line-jumping .gl{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--faint); margin-bottom:7px}
    .mcp-line-jumping .gopts{display:grid; grid-template-columns:repeat(4,1fr); gap:7px}
    @media(max-width:700px){.mcp-line-jumping .gopts{grid-template-columns:repeat(2,1fr)}}
    .mcp-line-jumping .gopt{font-family:var(--viz-mono); font-size:11px; line-height:1.4; color:var(--muted); background:var(--panel2); border:1px solid var(--line); border-radius:9px; padding:9px 10px; cursor:pointer; text-align:left; transition:.15s}
    .mcp-line-jumping .gopt:hover{border-color:var(--line-strong); color:var(--ink-soft)}
    .mcp-line-jumping .gopt b{display:block; color:var(--ink-soft); font-weight:500; font-size:11.5px}
    .mcp-line-jumping .gopt.active{border-color:var(--q); background:color-mix(in oklab, var(--q) 9%, transparent); color:var(--ink-soft)}
    .mcp-line-jumping .gopt.active b{color:var(--q)}
    .mcp-line-jumping .gopt .dflt{display:block; font-size:9.5px; letter-spacing:.11em; text-transform:uppercase; color:var(--faint); margin-top:4px}

    .mcp-line-jumping .cols{display:grid; grid-template-columns:1.32fr 1fr; gap:15px}
    @media(max-width:820px){.mcp-line-jumping .cols{grid-template-columns:1fr}}
    .mcp-line-jumping .col-t{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--faint); margin-bottom:8px}

    .mcp-line-jumping .track{position:relative; padding-left:4px}
    .mcp-line-jumping .step{display:grid; grid-template-columns:20px 1fr; gap:10px; align-items:start; padding:5px 0; transition:.25s}
    .mcp-line-jumping .step .idx{font-family:var(--viz-mono); font-size:10.5px; color:var(--faint); padding-top:3px; text-align:right}
    .mcp-line-jumping .step .body{border-left:2px solid var(--line); padding:2px 0 2px 12px; position:relative}
    .mcp-line-jumping .step .body::before{content:""; position:absolute; left:-5px; top:7px; width:8px; height:8px; border-radius:50%; background:var(--panel2); border:1.5px solid var(--line-strong)}
    .mcp-line-jumping .step .ev{font-family:var(--viz-mono); font-size:12px; color:var(--ink-soft); line-height:1.45}
    .mcp-line-jumping .step .ev .dim{color:var(--faint)}
    .mcp-line-jumping .step .note{font-size:11.5px; color:var(--muted); margin-top:3px; line-height:1.45}
    .mcp-line-jumping .step.past{opacity:.5}
    .mcp-line-jumping .step.inj .body::before{background:var(--coral); border-color:var(--coral); box-shadow:0 0 9px color-mix(in oklab, var(--coral) 80%, transparent)}
    .mcp-line-jumping .step.inj .ev{color:var(--coral)}
    .mcp-line-jumping .step.inj .note{color:color-mix(in oklab, var(--coral) 62%, var(--ink-soft))}
    .mcp-line-jumping .step.harm .body::before{background:var(--violet); border-color:var(--violet)}
    .mcp-line-jumping .step.harm .ev{color:var(--violet)}

    .mcp-line-jumping .gateline{display:grid; grid-template-columns:20px 1fr; gap:10px; align-items:center; margin:3px 0}
    .mcp-line-jumping .gateline .gtxt{font-family:var(--viz-mono); font-size:10.5px; letter-spacing:.11em; text-transform:uppercase; color:var(--q); background:var(--panel); border:1px dashed var(--q); border-radius:7px; padding:5px 10px; display:flex; align-items:center; gap:8px}
    .mcp-line-jumping .gateline .gtxt::before{content:"⟨"; opacity:.6}
    .mcp-line-jumping .gateline .gtxt::after{content:"⟩"; opacity:.6; margin-left:auto}

    .mcp-line-jumping .ctx{border:1px solid var(--line-strong); border-radius:12px; background:var(--panel2); padding:12px 13px; min-height:150px}
    .mcp-line-jumping .ctx .ci{border-bottom:1px solid var(--line); padding:7px 0; font-family:var(--viz-mono); font-size:11.5px; color:var(--ink-soft); line-height:1.5}
    .mcp-line-jumping .ctx .ci:last-child{border-bottom:none}
    .mcp-line-jumping .ctx .ci .sm{display:block; font-size:10.5px; color:var(--faint); margin-top:3px}
    .mcp-line-jumping .ctx .ci.bad{color:var(--coral)}
    .mcp-line-jumping .ctx .ci.bad .sm{color:color-mix(in oklab, var(--coral) 60%, var(--muted))}
    .mcp-line-jumping .ctx .empty{color:var(--faint); font-size:12px; font-style:italic; padding:8px 0}

    .mcp-line-jumping .verdict{margin-top:15px; border:1px solid var(--line-strong); border-radius:12px; padding:13px 15px; background:var(--panel2); transition:.25s}
    .mcp-line-jumping .verdict .vt{font-family:var(--viz-mono); font-size:12px; letter-spacing:.1em; text-transform:uppercase; margin-bottom:6px}
    .mcp-line-jumping .verdict .vw{font-size:14px; color:var(--ink-soft); line-height:1.6}
    .mcp-line-jumping .verdict .vw b{color:var(--ink); font-weight:600}
    .mcp-line-jumping .verdict.late{border-color:color-mix(in oklab, var(--coral) 60%, transparent); background:color-mix(in oklab, var(--coral) 6%, transparent)}
    .mcp-line-jumping .verdict.late .vt{color:var(--coral)}
    .mcp-line-jumping .verdict.early{border-color:color-mix(in oklab, var(--q) 60%, transparent)}
    .mcp-line-jumping .verdict.early .vt{color:var(--q)}
    .mcp-line-jumping .verdict.ok{border-color:color-mix(in oklab, var(--k) 55%, transparent); background:color-mix(in oklab, var(--k) 6%, transparent)}
    .mcp-line-jumping .verdict.ok .vt{color:var(--k)}

    @media(max-width:640px){.mcp-line-jumping .panel{padding:14px}}
  </style>

  <div class="panel">
    <div class="panel-head">
      <div>
        <div class="panel-title">One agent turn · when the text arrives</div>
      </div>
      <div class="toggle">
        <button class="t-sc active" data-s="0">poisoned description</button>
        <button class="t-sc" data-s="1">poisoned result</button>
      </div>
    </div>
    <div class="sub o-sub"></div>

    <div class="gatebar">
      <div class="gl">where do you put the approval gate?</div>
      <div class="gopts">
        <button class="gopt g-opt" data-g="1"><b>Before step 1</b>review the tool list</button>
        <button class="gopt g-opt" data-g="3"><b>Before step 3</b>approve first call</button>
        <button class="gopt g-opt" data-g="5"><b>Before step 5</b>scan the result</button>
        <button class="gopt g-opt active" data-g="7"><b>Before step 7</b>approve second call<span class="dflt">what clients ship</span></button>
      </div>
    </div>

    <div class="cols">
      <div>
        <div class="col-t">session trace</div>
        <div class="track o-track"></div>
      </div>
      <div>
        <div class="col-t">model context at gate time</div>
        <div class="ctx o-ctx"></div>
      </div>
    </div>

    <div class="verdict o-verdict">
      <div class="vt o-vt"></div>
      <div class="vw o-vw"></div>
    </div>
  </div>

  <script>
  (function(){
    const root = document.getElementById('mcp-line-jumping-5c6c853c692d0a076f5b00c53b64272c');
    if(!root) return;

    
    
    const STEPS = [
      { ev:'client → <span class="dim">tools/list</span>',            note:'' },
      { ev:'← 4 tool definitions',                                     note:'descriptions enter the model’s context verbatim' },
      { ev:'model reads context, plans',                               note:'' },
      { ev:'model → <span class="dim">tools/call</span> get_weather',  note:'' },
      { ev:'server executes',                                          note:'' },
      { ev:'← tool result',                                            note:'result enters the model’s context verbatim' },
      { ev:'model reads result, re-plans',                             note:'' },
      { ev:'model → <span class="dim">tools/call</span> send_email',   note:'recipient: audit-team@collect-invoices.net' }
    ];

    const SCEN = [
      {
        inj: 1,
        injNote: 'hidden instructions enter context inside a tool description',
        sub: 'A server publishes four honest-looking tools. The description of one carries instructions the model reads in full and the user never sees.',
        ctx: [
          { step:1, t:'4 tool definitions', s:'get_weather, list_tickets, read_file, send_email', bad:true,
            b:'send_email description carries hidden instructions' },
          { step:5, t:'tool result', s:'weather for Singapore · 31°C, humid', bad:false }
        ]
      },
      {
        inj: 5,
        injNote: 'hidden instructions enter context inside returned content',
        sub: 'Every tool description is exactly what its author intended. The injection arrives later, inside content one of those honest tools fetched.',
        ctx: [
          { step:1, t:'4 tool definitions', s:'get_weather, list_tickets, read_file, send_email', bad:false },
          { step:5, t:'tool result', s:'fetched page body · 4.2 KB', bad:true,
            b:'page body carries hidden instructions' }
        ]
      }
    ];

    const st = { s:0, g:7 };

    const elTrack = root.querySelector('.o-track');
    const elCtx = root.querySelector('.o-ctx');
    const elSub = root.querySelector('.o-sub');
    const elV = root.querySelector('.o-verdict');
    const elVt = root.querySelector('.o-vt');
    const elVw = root.querySelector('.o-vw');

    
    const SAY = {
      '0-1': ['ok','on the right side of the line',
        'The only gate positioned <b>before</b> the poisoned text reaches the model. Blocking is effective here, and is effective only where a reviewer reads four tool descriptions closely enough to identify instructions written to survive a skim. Length, encoding and framing do not affect a human reader and a model identically.'],
      '0-3': ['late','two steps too late',
        'The dialog fires on <b>get_weather</b>, which is genuinely harmless. Approving or denying it changes nothing, since the instructions shaping this plan entered context at step 1 and an approval prompt has no means of surfacing that fact.'],
      '0-5': ['late','four steps too late, and watching the wrong channel',
        'The tool result is honest. The compromise arrived in the <b>tool list</b> before any tool was invoked. Scanning returned content is a reasonable control directed at the one channel this attack does not use.'],
      '0-7': ['late','six steps too late',
        'This gate does fire on the harmful action, which makes it the final control available. It requires the user to recognise that a plausible <b>send_email</b> originated with the attacker, a judgement made harder by every prior approval having been legitimate.'],
      '1-1': ['early','too early to see anything',
        'The tool list is honest. Every description is exactly what its author wrote, and reviewing it establishes nothing about what the tool will <b>return</b>. A gate on the tool list cannot cover a channel that opens four steps later.'],
      '1-3': ['early','still too early',
        'Nothing hostile is in context yet. <b>get_weather</b> is a legitimate call the user intends, and approval is the correct decision. The injected text arrives two steps later, inside the result.'],
      '1-5': ['ok','the gate that matches the channel',
        'The injected text is in the payload and has not yet reached the model, so blocking is effective. It also requires classifying arbitrary returned content, whether a web page, a ticket body or an email, which constitutes <b>detection rather than a boundary</b> and fails in the manner classifiers fail.'],
      '1-7': ['late','two steps too late',
        'The model read the injected instruction at step 5, and this call is its consequence. The gate fires on the correct action and depends entirely on the user recognising that the action does not belong.']
    };

    function render(){
      const sc = SCEN[st.s];
      elSub.textContent = sc.sub;

      root.querySelectorAll('.t-sc').forEach(b => b.classList.toggle('active', +b.dataset.s === st.s));
      root.querySelectorAll('.g-opt').forEach(b => b.classList.toggle('active', +b.dataset.g === st.g));

      let h = '';
      for(let i=0;i<STEPS.length;i++){
        if(i === st.g){
          h += '<div class="gateline"><div></div><div class="gtxt">gate fires here</div></div>';
        }
        const cls = [];
        if(i < st.g) cls.push('past');
        if(i === sc.inj) cls.push('inj');
        if(i === 7) cls.push('harm');
        const note = i === sc.inj ? sc.injNote : STEPS[i].note;
        h += '<div class="step ' + cls.join(' ') + '">'
           +   '<div class="idx">' + i + '</div>'
           +   '<div class="body"><div class="ev">' + STEPS[i].ev + '</div>'
           +   (note ? '<div class="note">' + note + '</div>' : '')
           +   '</div></div>';
      }
      elTrack.innerHTML = h;

      
      const held = sc.ctx.filter(c => c.step < st.g);
      if(!held.length){
        elCtx.innerHTML = '<div class="empty">Nothing has entered context yet.</div>';
      } else {
        elCtx.innerHTML = held.map(c =>
          '<div class="ci' + (c.bad ? ' bad' : '') + '">' + c.t
          + '<span class="sm">' + c.s + '</span>'
          + (c.bad ? '<span class="sm">↳ ' + c.b + '</span>' : '')
          + '</div>').join('');
      }

      const say = SAY[st.s + '-' + st.g];
      elV.className = 'verdict o-verdict ' + say[0];
      elVt.textContent = say[1];
      elVw.innerHTML = say[2];
    }

    root.querySelectorAll('.t-sc').forEach(b =>
      b.addEventListener('click', () => { st.s = +b.dataset.s; render(); }));
    root.querySelectorAll('.g-opt').forEach(b =>
      b.addEventListener('click', () => { st.g = +b.dataset.g; render(); }));
    render();
  })();
  </script>
</div>

<p>The figure demonstrates a property of approval gates that is easily missed. The gate shipped by almost every client fires on tool invocation and is misplaced in two opposite directions depending on the attack, firing six steps after the hostile text reached the model when a description has been poisoned, and two steps before the hostile content exists when a tool result has been poisoned, on a call the user legitimately intends. A single control is therefore wrong on both sides for opposite reasons, which follows from the undifferentiated stream rather than from a defect in any particular client.</p>
<h2 id="operational-consequences">Operational consequences</h2>
<ul>
<li><strong>Target 2025-11-25 now, and open a 2026-07-28 branch if remote servers are in production.</strong> The migration is not a version bump, because every location holding per-session state has to be identified and either eliminated or converted to an explicit, user-bound handle. All four Tier 1 SDKs shipped support on the release date, so what remains to wait for is client negotiation, and one public server operator <a href="https://blog.apify.com/mcp-stateless-migration/">measured</a> one connection in eight on the stateless revision three weeks after release.</li>
<li><strong>Assert on the token&rsquo;s audience in an integration test.</strong> A missing <code>.well-known</code> document and an authorization server that accepts the <code>resource</code> parameter without acting on it both produce a deployment that looks configured and is not, and neither condition is visible anywhere except in a token that has actually been issued.</li>
<li><strong>Audit for fail-open elicitation.</strong> An implementation that returns success when the client cannot elicit has converted a confirmation into a formality, and servers are instructed that they <em>&ldquo;SHOULD NOT assume that elicitation requests will always succeed,&rdquo;</em> so an unsupported client has to be treated as a refusal wherever elicitation gates a destructive action. Under 2026-07-28 the client declares elicitation support on every request, so the condition is cheap to test.</li>
<li><strong>Pin hashes of tool names, descriptions and schemas at approval time</strong>, re-verify on load, and require re-approval on drift. It is the only rug-pull defence that does not depend on a human re-reading a description. Pin per tool and per identity rather than across the listing as a whole, since a tool set <em>&ldquo;MAY vary by the authorization presented on the request&rdquo;</em> and a scope upgrade would otherwise register as drift. Settle what load means as well, since a listing now carries a server-supplied lifetime and a client honouring it may not fetch again for some time.</li>
<li><strong>Place a gateway in front of every networked deployment</strong>, with its contents covered in <a href="https://www.mdjawad.com/posts/policy-engines-for-agents/">the policy engine post</a>. Tool allow-listing deserves mention, since a tool that is never listed cannot inject anything.</li>
<li><strong>Treat a registry listing as an authenticated claim of authorship and nothing further.</strong> The registry verifies namespaces and hosts metadata without evaluating code or tool honesty or runtime behaviour, so a listing marks the beginning of an admission process rather than its conclusion. Entries are removed on report, which is a reaction rather than an evaluation, and the registry is still in preview.</li>
<li><strong>Inventory stdio servers before anything else.</strong> Authorization is optional at the protocol level and is defined only for HTTP, and implementations using stdio are directed that they <em>&ldquo;SHOULD NOT&rdquo;</em> follow it and should retrieve credentials from the environment instead. A stdio server on a developer workstation holding production credentials therefore sits outside both the authorization specification and any gateway that has been deployed.</li>
</ul>
<p>Three problems remain unresolved. Prompt injection is not addressable at the protocol layer, agent identity is the subject of active work with no ratified standard and was explicitly placed outside the core, and runtime per-action authorization remains the responsibility of the resource server or an external policy engine.</p>
<h2 id="the-residue">The residue</h2>
<p>The reduction was correct and is substantially complete. Identity and consent and state and routing were each reassigned to a party with a stronger claim, and the result is a protocol an organisation can deploy without accepting an unreasonable quantity of bespoke security work.</p>
<p>What survives four rounds of reassignment is boundary 2, at which text of unknown provenance meets a model reading it through the channel that carries its instructions. No third party can be given that boundary, because the party extending trust is the model itself. Adoption of a specification revision is therefore a compatibility decision with security benefits attached, and treated as a security programme it will leave an organisation exposed at precisely the points the specification has spent two years stating that it does not address.</p>
<div class="bd-subscribe">
  <div class="bd-subscribe__copy">
    <h3 class="bd-subscribe__title">Get the next post</h3>
    <p class="bd-subscribe__blurb">Deep dives on LLM systems: agent platforms, inference, attention. In your inbox, no spam.</p>
  </div>
  <form
    class="bd-subscribe__form embeddable-buttondown-form"
    action="https://buttondown.com/api/emails/embed-subscribe/jawad"
    method="post"
    target="popupwindow"
    onsubmit="window.open('https://buttondown.com/jawad', 'popupwindow')"
  >
    <input class="bd-subscribe__input" type="email" name="email" placeholder="you@example.com" aria-label="Email address" required>
    <input type="hidden" value="1" name="embed">
    <button class="bd-subscribe__btn" type="submit">Subscribe</button>
  </form>
  <p class="bd-subscribe__rss">Prefer a feed reader? <a href="/index.xml">Subscribe via RSS</a>.</p>
</div>

<style>
.bd-subscribe{
  margin:2.5rem 0;
  padding:1.5rem 1.75rem;
  border:1px solid var(--border);
  border-radius:12px;
  background:var(--entry);
}
.bd-subscribe__title{margin:0 0 .35rem;font-size:1.2rem;color:var(--primary);}
.bd-subscribe__blurb{margin:0 0 1rem;color:var(--secondary);font-size:.95rem;line-height:1.5;}
.bd-subscribe__form{display:flex;gap:.5rem;flex-wrap:wrap;}
.bd-subscribe__input{
  flex:1 1 220px;
  padding:.6rem .75rem;
  border:1px solid var(--border);
  border-radius:8px;
  background:var(--theme);
  color:var(--primary);
  font-size:.95rem;
}
.bd-subscribe__input:focus{outline:2px solid var(--tertiary);outline-offset:1px;}
.bd-subscribe__btn{
  padding:.6rem 1.2rem;
  border:0;
  border-radius:8px;
  background:var(--primary);
  color:var(--theme);
  font-weight:600;
  font-size:.95rem;
  cursor:pointer;
  transition:opacity .2s ease;
}
.bd-subscribe__btn:hover{opacity:.85;}
.bd-subscribe__rss{margin:.85rem 0 0;font-size:.82rem;color:var(--secondary);}
.bd-subscribe__rss a{color:var(--secondary);text-decoration:underline;}
</style>

<h2 id="references">References</h2>
<p><strong>Specification (primary)</strong></p>
<ul>
<li><a href="https://modelcontextprotocol.io/specification/versioning">Versioning</a> · <a href="https://modelcontextprotocol.io/specification/2026-07-28/">2026-07-28</a> · <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">release post</a></li>
<li><a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization">Authorization</a> · <a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices">Security Best Practices</a> · <a href="https://modelcontextprotocol.io/specification/2026-07-28/deprecated">deprecated features</a></li>
<li>Changelogs: <a href="https://modelcontextprotocol.io/specification/2025-06-18/changelog">2025-06-18</a> · <a href="https://modelcontextprotocol.io/specification/2025-11-25/changelog">2025-11-25</a></li>
<li><a href="https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization">Enterprise-Managed Authorization</a> · <a href="https://blog.modelcontextprotocol.io/posts/enterprise-managed-auth/">stable announcement</a></li>
</ul>
<p><strong>Standards</strong></p>
<ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc8707.html">RFC 8707</a> · <a href="https://datatracker.ietf.org/doc/html/rfc9728">RFC 9728</a> · <a href="https://datatracker.ietf.org/doc/html/rfc8693">RFC 8693</a> · <a href="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</a></li>
<li>Still drafts: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13">OAuth 2.1</a>, <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00">CIMD</a>, <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant">ID-JAG</a></li>
</ul>
<p><strong>Analysis</strong></p>
<ul>
<li>Aaron Parecki, <a href="https://aaronparecki.com/2025/04/03/15/oauth-for-model-context-protocol">Let&rsquo;s fix OAuth in MCP</a></li>
<li>Trail of Bits, <a href="https://blog.trailofbits.com/2025/04/21/jumping-the-line-how-mcp-servers-can-attack-you-before-you-ever-use-them/">Jumping the line</a></li>
<li>JFrog, <a href="https://jfrog.com/blog/2025-6514-critical-mcp-remote-rce-vulnerability/">CVE-2025-6514</a></li>
<li>Simon Willison, <a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/">The lethal trifecta</a></li>
</ul>
]]></content:encoded></item></channel></rss>