MODELMARKET.DEV / BLOG
← All posts

7 Nov 2026 · 5 min read

An MCP tool set can gain a digest without changing

#mcp#security#opensource#observability

A schema default of 0.5 used to be enough for HISTOR to leave an MCP server's entire tool set without a digest. After a change to the measurement rules, the same declarations could receive one. The server's author did not have to edit anything.

That left an awkward historical record: "not digestible" could still describe a decision made under rules the observer no longer used. The label did not say which revision had declined the set.

HISTOR logs the tool definitions MCP servers advertise. Its MTL profile specifies a canonical representation of those definitions and the digest computed from it. MTL is our draft labeling profile; it is not a standard of the MCP project.

Two recent revisions addressed different parts of the problem. Revision 1.1, deployed on October 31, expanded which tool sets could be digested. Revision 1.2, deployed on November 6, records why a tool-set digest is absent and which rules made that decision.

The rules changed before the records did

MTL 1.0 declined schemas containing fractions, exponent notation, or integers outside the safe integer range. It also declined empty tool sets and sets with repeated tool names.

Revision 1.1 reads numbers as IEEE 754 doubles and writes them in the RFC 8785 canonical form. It handles empty sets and repeated names too. Definitions that the older rules declined could therefore acquire a digest without any change on the server.

Digests already produced under 1.0 stayed unchanged, so the descriptor's mtl identifier remained "1". That preserved existing comparisons, but it did not tell a later reader which revision had produced an old refusal.

In the Agent Control Standard discussion, avp9-nexus proposed keeping two absence cases apart, with a reason and profile identifier attached. That distinction fits the records HISTOR actually has:

Reason What the observer recorded
not_observed No tool set was read for this observation
not_digestible A set was read, but the applicable profile declined to digest it

A failed read calls for investigation of the endpoint or access problem. A profile refusal calls for the refusal code and the rules that produced it. An empty list read successfully belongs to neither absence case: since 1.1, the empty set can have a digest.

Our follow-up records the profile revision as well. New labels issued under 1.2 name profileRevision: "1.2.0"; a read set declined by the profile carries a toolSet.noDigest reason and the profile's code where known. Revision 1.2 changes no digest calculation.

A null revision can be the honest answer

During the November 6 rollout, a server-detail response exposed this historical reason object:

{
  "reason": "not_digestible",
  "code": "MTL-NUM-001",
  "profileRevision": null,
  "decidedAt": "2026-10-20T21:55:11Z"
}

The old label never stated its revision. The API leaves that field null rather than assigning today's rules to a decision made on October 20. decidedAt belongs to the historical observation, not to the upgrade that exposed the extra fields.

Continuity labels need to preserve both sides of a comparison. A failed current read can carry not_observed under noDigest.current; a prior set declined by the profile has its reason on the prior side. The earlier revision is copied only if the earlier label actually named it. Updating the issuer does not fill gaps in an old assertion.

A missing tool-set digest also does not erase the label's signature or every other digest it contains. Neither absence reason is a verdict on server safety. The signature attributes the assertion to an issuer; it does not independently prove that retrieval occurred or when.

Letting unchanged packages run again

A scheduler watching only package versions would miss the change. It has already run that release, the release has not moved, and its old result says there is no tool-set digest.

HISTOR now makes a package eligible for another run when an older MTL revision declined its set. If the current revision still declines it, the recorded decision settles that case until relevant inputs change. The revision marker is excluded from security events, so an observer upgrade is not reported as a security event attributed to the package.

The scheduler regression checks this unchanged-version path. Label tests cover the two reasons, refusal codes, signatures with the new fields, and comparisons whose earlier label lacks a revision. Before the November 6 production switch, a disposable instance exercised label generation and both API views without network access; outside checks followed the deployment.

The distinction is visible in the public API: one unobserved entry and one undigested entry. Both returned their corresponding noDigest.reason when checked on November 7. These are reasons attached to particular measurements, so the date and profile revision matter as much as the word "missing."

We build HISTOR. AI assistance was used for this article and the implementation work it describes.

Questions, corrections, other numbers? The discussion is on dev.to.

Discuss on dev.to

More field notes