← All docs

Sanction and the MCP 2026-07-28 specification

The Model Context Protocol's 2026-07-28 revision is its largest change since launch: the protocol core goes stateless, Roots, Sampling, and protocol Logging are deprecated, authorization is hardened, and Tasks and MCP Apps land as first-class extensions.

This page states where sanction-mcp stands against it, what changed on our side, and — because the headline reads worse than the substance — what the revision's "authorization hardening" does and does not cover.

MCP checks the badge. Sanction decides what the badge opens.

The revision's auth work is authentication: proving who is calling. RFC 9207 issuer validation, client credentials bound to their issuing authorization server, Client ID Metadata Documents replacing Dynamic Client Registration. All of it answers is this caller who it claims to be?

Sanction sells authorization: whether this agent may take this action, under whose budget, against which policy revision, and who signs when it crosses the line. The spec is not moving into the policy layer, and the two compose rather than compete — a hardened token tells you the caller is real; it tells you nothing about whether a $4,000 charge to an unfamiliar merchant should go through at 2am.

One further note for stdio servers: the revision explicitly directs them away from the OAuth framework — implementations "using STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment." That is exactly how sanction-mcp has always worked.

Conformance status

sanction-mcp is a stdio server. Most of the revision's headline changes are Streamable-HTTP transport concerns owned by the SDK, not by server authors.

ChangeApplies to sanction-mcp?Status
initialize handshake removedTransport-level (SDK)No server change. The design rule binds stdio too — see Statelessness below.
Mcp-Method / Mcp-Name routing headersStreamable HTTP onlyNot applicable to stdio.
ttlMs / cacheScope on tools/listEmitted by the SDKNot yet emitted by SDK 1.x. Our tool list is static and deterministically ordered, so it is genuinely cacheable once the SDK supports it.
Tool schemas as JSON Schema 2020-12YesSchema bodies are 2020-12-clean and gated by tests. One SDK-owned gap — see Schema dialect.
Roots / Sampling / Logging deprecatedYesZero dependencies on all three. Gated by tests.
OAuth / OIDC hardeningNoThe spec directs stdio servers to environment credentials, which is what we do.
Protocol Logging → standardized tracingYesShipped — see Trace context.

Everything in this table is enforced by tests/mcpConformance.test.ts, which runs in CI on every push. It is a regression gate, not a claim: if the tool surface drifts out of conformance, the build fails.

Statelessness

The revision makes statelessness a protocol invariant, and it binds stdio explicitly: "an open connection, such as a STDIO process, is not a conversation or session." Servers must not treat process identity as session identity.

sanction-mcp holds no per-process mutable state. Configuration comes from the environment at startup; every tool call is a self-contained POST to the Sanction API carrying its own credentials; nothing is cached between calls. The escalation loop — the one place a governed action spans multiple round trips — is already modelled the way the revision prescribes for cross-request state: an explicit identifier (request_id, then a one-use grant_id) that the caller passes back on each subsequent request. There is no hidden continuation to lose.

The conformance test pins this by exercising a tool and then asserting the tool surface is byte-identical afterward.

Schema dialect

The revision defaults an absent $schema to JSON Schema 2020-12 and requires clients to support that dialect. Explicit alternative dialects remain legal.

@modelcontextprotocol/sdk 1.29 stamps "$schema": "http://json-schema.org/draft-07/schema#" on every emitted tool schema. This is hard-coded — server/zod-json-schema-compat.js maps an absent target to draft-7 with no override — and it affects every server built on the v1 TypeScript SDK, not just this one.

The practical exposure is narrow but real: a strict, 2020-12-only client is within its rights to reject a draft-07-tagged tool. We verified the schema bodies are already 2020-12-clean — they compile and validate identically under a strict 2020-12 validator with the dialect tag removed, including rejecting a negative amount_usd and an out-of-enum action. So the gap is the tag, not the schemas. The fix belongs upstream in the SDK or in the v2 SDK migration; until then the test pins the current output so a change is a decision, not a surprise.

Trace context

The revision deprecates protocol-level Logging and points observability at OpenTelemetry, reserving three _meta keys — traceparent, tracestate, and baggage — as an explicit exception to its reverse-DNS prefix rule.

sanction-mcp now reads W3C trace context off each tool call and forwards it to the Sanction API as the same-named HTTP headers. If your MCP host propagates trace context, a Sanction decision can be correlated with the caller's own trace: the agent run that requested the spend, the decision that governed it, and the approval that unblocked it become one correlated story across your observability stack and our audit log.

// The host's tools/call — trace context rides in _meta
{
  "method": "tools/call",
  "params": {
    "name": "sanction_authorize",
    "arguments": { "action": "purchase", "amount_usd": 42, "merchant": "Anthropic", "category": "software" },
    "_meta": { "traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01" }
  }
}

Three properties are deliberate:

  • Tracing is never load-bearing. Trace context is diagnostic. It does not participate in authorization, and a missing or malformed value never changes a decision — a spend authorization must not fail because a header was wrong.
  • Every field is validated. These values arrive from the host, which is untrusted input that we turn into outbound HTTP headers. Each is checked against its W3C grammar and dropped on any mismatch; the grammars exclude CR, LF, and NUL, so a malformed value cannot smuggle a second header into the request. tracestate and baggage only travel alongside a valid traceparent.
  • Carried, not yet signed. Trace context is correlation metadata, not decision evidence. Sanction's tamper-evident audit export commits to an explicit allowlist of fields (lib/auditChain.ts), and trace identifiers are not on it. Binding the customer's trace id into the signed chain changes the hash of every decision and is therefore an audit-format version bump, tracked separately — see below.

What is next, and what it is not

Two parts of the revision are genuine product openings rather than compliance work, and both are arcs rather than same-week slices:

Tasks → escalation. The Tasks extension is a near-exact match for Sanction's escalated state. Its lifecycle includes an input_required status with an inputRequests map resolved via tasks/update, and the spec names "human-in-the-loop workflows… approval gates" as a primary use case. A request that pauses, waits for a human, and mints a one-use grant on approval is a resumable long-running task. Implementing escalation as a first-class MCP Task would make it work natively in every compliant host, replacing the current poll loop (sanction_check_authorization) with the protocol's own.

MCP Apps → the approval card. MCP Apps lets a server return sandboxed, server-rendered UI. Today escalation pulls the human out to the Sanction console; with MCP Apps the amount, agent, category, and approve/deny controls render where they already are. That is a direct attack on approval latency, which is the metric that decides whether a team leaves the guardrail switched on.

Both depend on the v2 TypeScript SDK (@modelcontextprotocol/server@beta), which is in beta and renames the server API. sanction-mcp governs real money in other people's stacks; it does not move to a beta dependency on release week. The migration, the Tasks-native escalation loop, the MCP Apps approval card, and the signed-trace audit format are tracked in docs/BACKLOG.md.

Verifying it yourself

npx vitest run tests/mcpConformance.test.ts
node scripts/test-mcp-install.mjs

The first is the conformance gate. The second is the end-to-end install test: it spawns both the local bundle and the published npm package over stdio and asserts they serve the identical tool surface.