Extension

Attested Tool-Server Admission

How a host decides which tool servers it may connect to, at what sensitivity, and which of their tools are in bounds

Source · Built from 40123d1 · 2026-09-29

omcp standardizes how a host and a tool server exchange messages. It does not, on its own, say anything about trust: a host reads a server's self-declared tool list and dispatches calls to it with no notion of which servers it may use, at what sensitivity, or which of that server's tools are in bounds.

Attested Tool-Server Admission (ATSA) adds that layer. A server publishes a small, offline-signed clearance assertion; a host verifies it against a locally pinned trust root before any tool dispatch; and admitting a server is kept deliberately separate from authorizing its tools.

Authors

Alfredo Metere (@enclawed), Enclawed, Inc., with Syed Maaz Ahmed (@MaazAhmed47), Interlock, and Christopher Hopley, AlgoVoi.

The shape of the problem

Three decisions get conflated in an unattested deployment, and separating them is the point of this extension:

Question Answered by
Is this server one I am permitted to talk to? Admission — verifying the attestation
At what sensitivity may I talk to it? The clearance level it asserts
Which of its tools may I actually call? Authorization — the host's per-server allow-list

A server that is admitted is not thereby authorized. That distinction is what stops a compromised or over-eager server from widening its own reach by advertising more tools.

An implementation running in compatibility mode does not use this at all: admission is not evaluated, and the outcome says so rather than reporting a pass.

Discovery

A server advertises an attestation in either of two interoperable ways:

  1. Well-known resource. The server SHOULD serve the document at /.well-known/mcp-attestation, on the same origin as its omcp endpoint, over TLS.
  2. In band. The server MAY return it in the initialize result under an experimental capability key.

A conforming host MUST attempt the first, MAY attempt the second, and MUST treat the absence of both as unattested.

The Server Attestation Document

A JSON object. Required fields:

Field Meaning
v Document version, "MAJOR.MINOR". "1.0" for this revision.
id Stable server identity.
publisher The entity publishing the server.
version Server version.
clearance Canonical name or alias of a level in a named, totally ordered scheme.
capabilities Must contain "mcp-server".
signerKeyId Identifies a key in the host's trust root.
signature Base64 detached signature over the canonical body.

Optional, and part of the signed body when present: netAllowedHosts, which binds the document to specific origins, and verification.

{
  "v": "1.0",
  "id": "urn:omcp:server:weather",
  "publisher": "Example Tools Ltd",
  "version": "2.3.1",
  "clearance": "internal",
  "capabilities": ["mcp-server"],
  "netAllowedHosts": ["https://tools.example.com"],
  "signerKeyId": "key-prod-2026",
  "signature": "mRheMehBUkfVed42+i9Axd6tGiKbZfGj1kYGu6Ebbhmz..."
}

What gets signed

The signature covers the canonical body: every registered field except signature, with object keys sorted, array members sorted, and an absent signerKeyId serialized as null.

Two consequences follow, and both are deliberate:

The reference profile uses Ed25519.

Host verification rules

A host MUST evaluate these in order and MUST deny on the first failure. The order is what makes a denial reason meaningful.

# Rule Reason on failure
0 The document's MAJOR version is one the verifier understands unsupported_version
1 The document parses and capabilities contains "mcp-server" not_mcp_server
2 signerKeyId and signature are present unsigned
3 signerKeyId resolves to a key in the trust root signer_not_trusted
4 That key's validity window includes now signer_expired
5 The key is approved for the asserted clearance signer_not_approved
6 The signature verifies over the canonical body bad_signature
7 clearance dominates the host's required level below_required
8 If netAllowedHosts is non-empty, the connected origin is a member host_not_bound

Rule 5 is checked against the canonical level name, so an alias cannot be used to widen a key's scope.

Posture

What a host does with an unattested or failing server is policy, not protocol:

That second clause is the one that matters. Silent downgrade is how attestation schemes become decorative.

Tool authorization

Admitting a server MUST NOT be read as authorizing all of its tools. A host SHOULD keep a per-server allow-list and MUST deny a tools/call for any tool absent from it before any network dispatch, regardless of what tools/list advertised. A denial uses the reason tool_not_admitted.

Trust establishment

How the trust root is provisioned, pinned, and sealed is deployment policy, and deliberately out of scope. A profile may anchor it in a pinned key set, X.509 chains, SPIFFE SVIDs, or a sigstore-style transparency log; the verification rules are unchanged either way.

Compatibility

v versions the document as "MAJOR.MINOR". A verifier MUST reject a document whose MAJOR it does not understand, and MUST accept a higher MINOR within a MAJOR it does: minor revisions are additive only, so fields one introduces are ignorable by an older verifier under the same rule that governs unknown fields. The version is a string because JSON does not distinguish 1.0 from 1. The extension is purely additive: an unextended host never fetches the document, an unextended server never sees the host-side allow-list, and a host MUST interoperate with unattested servers under its posture. That is what permits incremental rollout rather than a flag day.

Implementing it

A reference implementation ships in this repository, under extensions/atsa: canonicalization, the eight verification rules, tool authorization, and a JSON Schema for the document.

It comes with conformance vectors covering every rule and every denial reason, committed as data together with the trust root they verify against, so an implementation in any language can be checked against the same cases:

npm test

The vectors are generated from fixed key seeds and are byte-for-byte reproducible — regenerate them and the diff should be empty.