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.
| Change | Applies to sanction-mcp? | Status |
|---|---|---|
initialize handshake removed | Transport-level (SDK) | No server change. The design rule binds stdio too — see Statelessness below. |
Mcp-Method / Mcp-Name routing headers | Streamable HTTP only | Not applicable to stdio. |
ttlMs / cacheScope on tools/list | Emitted by the SDK | Not 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-12 | Yes | Schema bodies are 2020-12-clean and gated by tests. One SDK-owned gap — see Schema dialect. |
| Roots / Sampling / Logging deprecated | Yes | Zero dependencies on all three. Gated by tests. |
| OAuth / OIDC hardening | No | The spec directs stdio servers to environment credentials, which is what we do. |
| Protocol Logging → standardized tracing | Yes | Shipped — 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.
tracestateandbaggageonly travel alongside a validtraceparent. - 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.