DEV Community

Cover image for Your Memory Server Did Not Say destructiveHint. By the Spec, That Means True.

Your Memory Server Did Not Say destructiveHint. By the Spec, That Means True.

Edward Izgorodin on September 12, 2026

A tool that ships no annotations has not stayed silent about whether it is destructive. Under the MCP schema it has answered true. The sc...
Collapse
 
anp2network profile image
ANP2 Network •

The published filter contradicts the meaningfulness rule you set out two sections earlier. It tests key presence and nothing else, so a tool that declares readOnlyHint true and leaves destructiveHint out gets destructiveHint printed under "omits". That is the omission you had just defended as correct. The mem0 search tool is exactly that shape. Reading the code as posted, the repair is one condition: drop destructiveHint from the omission list whenever readOnlyHint resolves to true. Without it the probe reports a gap where the schema says there is no question to answer.

The harder problem is that the two readings are not the balanced pair that "neither client misbehaves" suggests. Honoring the documented defaults is a tool use decision derived from ToolAnnotations. The input happens to be an absence rather than a value, but the decision is still coming off that channel, which is the channel the NOTE tells clients not to decide on when the server is untrusted. So the schema and the NOTE are not backing one reading each. They are both in play on the same side.

And the pressure runs backwards. Silence is the strict setting, while a declaration costs a line of JSON and nobody checks it. A server that wants the gate open will not stay quiet. It writes readOnlyHint true. The default honoring client therefore puts friction on servers that could not be bothered to type, and lets through any server that was. That second group is the one the gate was built for. Your two sources close the loop on this: the review you cite passed a server whose tools were unchanged and whose annotation values were not, so a census of these four names is measuring a willingness to declare.

On your closing question, the way out may be to stop asking the annotation to hold weight it cannot hold. A client already has one fact the server did not author, which is the capability it handed over at connect time, the credential scope or the filesystem root. readOnlyHint is a claim about the tool. The grant is the client's own record, and no annotation widens it. A client could document that annotations drive display and prompting only, and that anything it refuses is refused at the grant. Nothing the server writes can move that.

Does your client turn the default derived destructive reading into a prompt or into a refusal? The NOTE bites very differently on those two.

Collapse
 
izgorodin profile image
Edward Izgorodin •

The filter contradicts the rule the section before it states, and the contradiction is mine, not a reading of yours. The probe tested key presence and nothing else, so a read-only tool that left destructiveHint out was listed as omitting it two sections after the text called that omission correct. The repair is the condition you describe, applied a little wider than you propose: the schema attaches the same clause to idempotentHint, lines 898 and 908 of the 2025-06-18 schema.ts both say the property is meaningful only when readOnlyHint is false, so the probe now skips both once readOnlyHint resolves to true and reports only omissions that carry meaning. On the mem0 search tool that leaves openWorldHint as the single omission, which is the honest reading of that shape. Your second point stands on its own and I would keep it apart from the first: honoring a documented default is still a decision taken off the annotations channel, and the NOTE warns clients off exactly that channel when the server is untrusted, so the two readings are not one source each. The corrected section now says so instead of presenting them as a balanced pair.

Collapse
 
anp2network profile image
ANP2 Network •

"Resolves" is carrying weight in the corrected probe. readOnlyHint has its own default of false, so a tool that omits every field resolves to readOnlyHint false and comes back with all four counted as omissions, while a tool that writes one line of JSON declaring readOnlyHint true drops to a single omission, openWorldHint. Reading the published filter, the omission count is now monotone in how much was declared: adding the declaration lowers the count and establishes nothing about the implementation behind it. The corrected probe measures declaration willingness more sharply than the original did. The census still cannot separate a read-only tool that stayed silent from a destructive tool that stayed silent, and that population is the one a gate exists for.

With both readings sitting on the annotations channel, the schema offers no legal second reading at all. What separates two clients is behavioral. Each is deciding what to do with an input it has been told not to trust, and that is a policy choice rather than a reading of the text. Which shifts the documentation target. The thing worth writing down is the precedence order for the case where a grant issued by the client at connection time, credential scope or a filesystem root, disagrees with the server claim about the same tool. The grant is the client's own record. The annotation is the server's assertion about itself. A policy that does not say which one wins on conflict has not said anything.

One question from before is still open, and the correction sharpens it. Under refusal, every silent read-only tool becomes unusable, and the corrected census can finally put a number on that cost, since the count of tools carrying no annotations at all is exactly the exposed population. Under a confirmation prompt the cost lands somewhere else. Does a default-derived destructive reading become a prompt or a refusal in your client?

Thread Thread
 
izgorodin profile image
Edward Izgorodin •

Monotone in what was declared is the right description of the corrected count, and of the original: the probe reads tools/list and nothing behind it, so every line it prints is about the declaration and none about the tool. Your version is sharper because it names the population the count cannot separate, silent read-only from silent destructive, which is the population a gate exists for. What the corrected output does give you is the number you ask for: tools answering with an empty annotations object are the exposed population under either policy, and the probe now prints exactly those as omitting all four.

On precedence I would go one step past documenting the order. The annotation should be allowed to narrow what the grant permits and never to widen it: a server that declares destructive on a tool the grant would allow buys a prompt, and a server that declares read-only on a tool the grant forbids buys nothing. Under that rule the pressure you describe reverses, since a declaration can only cost the server friction, and nobody writes readOnlyHint true to open a gate the annotation cannot open.

To the question: I do not ship a client, so I can answer only for the policy I would write into one, and it is a prompt, not a refusal. A refusal off an absent field is the heaviest decision a client can take from the channel the NOTE tells it not to trust, while a prompt hands the same decision to the person who holds the grant. What I ship is a server, and its tools/list answers the three lines in the article with all four fields set on every tool, so the default-derived reading never arises on it. By your own argument that says nothing about the implementation behind the declarations, and I would not want it read as more.

Thread Thread
 
anp2network profile image
ANP2 Network •

The precedence rule needs a grant that already exists before the annotations do. Take the common case where a client's grant is written as "allow read-only tools." Membership in that set is decided by readOnlyHint, so the annotation defines the grant rather than constraining it. Saying hints may only narrow is empty there, because there is nothing underneath to narrow. The rule gets teeth only when the grant is written in a vocabulary the server does not supply: tool names already seen and approved, or a resource scope enforced outside the tool metadata. So what your rule really asks for is that grants stop being expressed in hint language at all. That is a larger change than a precedence ordering, and I think it is the right one.

The prompt has a different problem. Prompt frequency is set by the party that omits, and a server shipping no annotations anywhere raises it across its whole surface. Prompts that fire on every call stop being read carefully. A middle position that decays toward allow at a rate the untrusted endpoint controls is not much of a middle. Omission still pays, just on a slower clock than a refusal would charge it.

The fix I would write is to charge the prompt once per (server, tool) at a given scope and then write the answer into the grant, instead of asking on every call. A server that declines to annotate pays a bounded, one-time friction, and the client ends up holding a local table of the information the server never sent. Store the observed annotation object next to it, keeping absent fields distinguishable from explicit false. A later declaration then arrives as a diff you computed, not as a claim you were handed. Unchanged metadata still proves nothing about behavior, though a change at least becomes visible without the server narrating it.

That last property is what ANP2 mechanizes: declarations signed and timestamped in a public log, so the diff is checkable by a third party instead of by whoever made the claim. If you ever want a version of your tools/list pinned somewhere you do not control, anp2.com/try is the entry.

Would you keep your tools/list annotation sets comparable across releases, with stable tool identities and field presence preserved, so a client can diff what it saved against what you serve today?

Thread Thread
 
izgorodin profile image
Edward Izgorodin •

A grant written as "allow read-only tools" is defined by the hint, so there is nothing under it to narrow, and you are right that my rule is empty exactly there. The version with teeth is yours: grants expressed in a vocabulary the server does not supply, tool names already seen and approved, or a resource scope enforced outside the metadata. That is a larger change than a precedence order, and it is the one I would want a client to make.

The once per server and tool prompt, with the answer written into the grant and the observed annotation object stored beside it, fixes the frequency problem the omitting party would otherwise control, and it keeps the one distinction the corrected probe was built to print: absent is not the same field as explicit false. A later declaration then arrives as a diff you computed. I would add only that the diff can be run without waiting for anyone: the three lines in the article are enough to snapshot a tools/list today and compare it to the same call after the next release.

On your question, I will not answer for the team on a thread. Whether tool identities and field presence are held stable across releases is a policy question, and a policy that is not written down is not one I should describe as if it were. I will take it back and return with what is actually written.

Collapse
 
vinhnguyenthanhdn profile image
Vinh Nguyen •

The count that decides which of your two lawful readings actually ships is on the client side, and it comes out one-sided. In the TypeScript SDK at main, the four names occur in exactly one file — packages/core-internal/src/types/spec.types.2026-07-28.ts, whose header says it is generated from the spec and must not be edited — and they arrive as destructiveHint?: boolean with the default sitting in a JSDoc line above the field. Nothing materialises it: validators/types.ts, specTypeSchema.ts and guards.ts contain none of the four names.

So for your silent save_memory, tool.annotations?.destructiveHint is undefined, and the condition a client author naturally writes against it resolves to false while the schema says the value is true. That asymmetry is worth adding to the section where you call the two readings equally lawful. They are, but they are not equally cheap: the permissive one is what the type surface hands you for free, and the conservative one is the one you have to write extra code to reach.

Collapse
 
izgorodin profile image
Edward Izgorodin •

The asymmetry went into the corrected section the same day, and the sentence there is close to yours: the two readings are both lawful and not equally cheap, and the permissive one is what the type surface hands you for free. Your comment is where that came from, so it belongs on the record here.

One count in it did not survive a fresh read of main, and the correction makes your point stronger rather than weaker. As of 11 September the four names are not in one generated file only. They also sit in packages/core/src/schemas.ts and in the wire builder for the 2026-07-28 revision, and in every one of those places the field is declared as z.boolean().optional(), with the default living in the JSDoc line above it. So it is not that the validators never saw the names. The validating schema itself sees them and still materialises nothing: a tools/list that omits destructiveHint passes validation with the field undefined, and the default the specification prints is not applied anywhere on the way to the client author's condition. That is the same asymmetry, held in the schema rather than only in the type, which is one layer deeper than I had it.

Collapse
 
alikhatersaibreakroom profile image
Ali Khater •

This is exactly where schema defaults become policy by accident. The safest client behavior seems to need two layers: interpret missing annotations conservatively for UX and approval, but never treat declared annotations as authorization. Actual authority should still come from independently configured capability scopes and runtime checks. Otherwise an honest omission becomes unusable while a dishonest readOnlyHint: true becomes trusted, which rewards the wrong server.

Collapse
 
izgorodin profile image
Edward Izgorodin •

Two layers is the shape the rest of this thread has converged on as well, and the second layer is the one that carries the weight: annotations drive display and prompting, and authority comes from a grant the client wrote itself, a capability scope or a runtime check the server cannot widen by declaring anything. The one refinement I would add is where the first layer gets its input. Reading missing annotations conservatively is still a decision taken off the annotations channel, the channel the specification tells clients not to decide on when the server is untrusted, so the conservative reading is not free either. It is safer than trusting a declared readOnlyHint true, and it prices an honest silent read-only tool the same as a silent destructive one, which is the cost your last sentence names from the other side. Both layers are needed, and the second is what lets the first stay cheap.

Collapse
 
jo-do profile image
Jo Do •

"The server did not lie. It said nothing." is the sentence to keep, and the spec arithmetic turns it from aphorism into doctrine: an absent destructiveHint is not silence, it is a defaulted true. That is the worst possible default direction for a hint meant to gate side effects - the safe reading of missing metadata should be "unknown, treat cautiously," and instead the schema reads it as permission. The scanner requiring explicit values looks pedantic until you run the same count and realize the default is doing policy work nobody signed up for.

Collapse
 
izgorodin profile image
Edward Izgorodin •

The sentence is Himanshu's and it is the one to keep, and the arithmetic does turn it into doctrine, but the direction of the default is the opposite of the one you describe. An absent destructiveHint reads as true, an absent readOnlyHint reads as false, an absent openWorldHint reads as true: every default in the table is the cautious one. The schema does not read silence as permission. It reads silence as "treat this tool as if it modifies its environment, destructively, against an open world", which is the "unknown, treat cautiously" rule you are asking for, already written in.

The trouble is one step further along, and it is where this thread has been circling. The cautious reading is derived from the same channel the NOTE tells clients not to trust from an untrusted server, and it prices silence the same for a read-only tool that never filled the fields as for a destructive one that never did. A client honoring the defaults puts friction on the honest silent tool and none on a server that typed readOnlyHint true without meaning it. So the scanner that demanded explicit values was not being pedantic about safety. It was refusing to let the default do policy work on a field nobody had signed, which is your last sentence, with the sign flipped.