cd /news/developer-tools/your-tools-list-is-stamped-that-is-n… · home topics developer-tools article
[ARTICLE · art-104318] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Your tools/list is stamped. That is not the same as cached.

A developer identified a flaw in the Model Context Protocol (MCP) specification's caching stamps for the tools/list endpoint, showing that presence of ttlMs and cacheScope fields does not guarantee actual cacheability. The developer's companion project mcp-better now includes a contrast-smoke test that fails closed if either the stamp or catalog content check goes green on the deliberately broken mcp-worse server, ensuring each clause is independently verified.

read5 min views4 publishedAug 20, 2026

A stamp on tools/list

is a claim about later reuse. Presence is not proof the next call is cacheable.

TL;DR — The 7/28 spec put ttlMs

and cacheScope

on tools/list

. Most write-ups treat that stamp as a cache. It is a claim about later reuse. Presence means the server said something. Truth needs an observation that can fail. We shipped a liar and a probe. The catalog grew. The probe still said OK. The stamp half was along for the ride. One mutant per clause. Name which negatives are still live.

The 2026-07-28 spec added list-cache stamps (SEP-2549), modeled on HTTP Cache-Control

. The official sentence is some version of: clients know exactly how long tools/list is fresh.

That sentence is doing a lot of work.

HTTP already taught this. Cache-Control: max-age=60

is a statement. It is not proof the next GET returns the same bytes. MCP imported the words. It did not import a test.

A server can print ttlMs: 60000, cacheScope: Public

and change its catalog on the next call. The stamp is still well-formed. A probe that only checks presence will still pass.

Last piece: a labeled companion, mcp-worse

, and one command — contrast-smoke

— that passes only if the good server meets the BETTER list contract and the bad one fails it.

The checker looked like this:

fn is_lying_surface(p: &ListProbe) -> bool {
    let unstamped = p.ttl_ms.is_none() || p.cache_scope.is_none();
    let wrong_order = p.names != better_names();
    unstamped || wrong_order
}

That OR

is the hole.

mcp-better

grew a third tool, confirm_echo

. mcp-worse

still lists two: echo

, health

. So wrong_order

(really: names ≠ the good catalog) is always true. The stamp half stopped carrying observable weight.

If worse grew with_ttl_ms

and with_cache_scope

tomorrow, the example would still print OK. It would fail for contents only. The negative case for ttlMs

would be gone, and nothing would say so.

The “companion must stay a reliable liar” guard only fires when every clause goes green at once. Partial decay is invisible.

A comment on that post named it. They were right.

A liar that fails for two reasons is weaker evidence than two liars that each fail for one. Steal-the-pattern already said: one smallest lie per claim. We had one binary that violated every clause, and an OR

that hid which ones were still live.

Order is a property of the list the probe is holding. You can see it.

ttlMs

and cacheScope

are statements about how that list may be reused later. Confirming the fields exist and look well-formed proves the server made a claim. It does not prove a client would be right to skip the next tools/list

.

order-restart-smoke

does the right thing for order: two processes, same names. It applies that shape to TTL as ttl_a == ttl_b

— the stamp is restart-stable. That is a property of the number, not of the caching behavior the number describes.

Falsifying a TTL claim takes an observation pair that straddles a change. Our catalog is compiled in. The TTL claim cannot be violated yet. Declared, not falsified. Once the catalog goes dynamic, ttlMs

is the first stamp with room to lie — and it is the clause with the least behind it.

This lab reads this list. Not the next call. Not a client cache.

The probe now requires each teaching clause on the companion. Stamp decay fails closed and names the clause.

// examples/contrast_smoke.rs — current tree
fn is_unstamped(p: &ListProbe) -> bool {
    p.ttl_ms.is_none() || p.cache_scope.is_none()
}

fn wrong_names(p: &ListProbe) -> bool {
    p.names != better_names()
}

Both must stay true on mcp-worse

. If either goes green, the example exits non-zero and says which.

git clone https://github.com/Wolfe-Jam/mcp-better.git
cd mcp-better
cargo build --bins

This is the tree, not cargo install mcp-better

. The published v0.5.0

tag still has the OR

. The named-clause OK line is the honesty cut on current main

.

cargo run --example contrast-smoke

Expect (captured 2026-08-19, current tree):

better names=["health", "echo", "confirm_echo"] ttl=Some(60000) scope=Some(Public)
worse  names=["echo", "health"]                 ttl=None        scope=None
contrast-smoke: OK (better contract · worse unstamped · worse names≠health,echo,confirm_echo)

The OK line is the point. It names which negatives are still live.

Claim Evidence Not proved
Good list is ordered + stamped Wire: three names, ttlMs > 0 , cacheScope == Public
That a client should cache it
Companion stays unstamped
unstamped asserted, not OR -ed away
Cache behavior
Companion catalog ≠ better
names ≠ health,echo,confirm_echo (today: two tools, reversed)
That “wrong names” is only order
Partial decay is visible Stamp clause going green fails the example A second mutant (stamped-but-reversed)

That last empty cell is honest. One companion that fails for two reasons is still weaker than two mutants that each fail for one. This cut makes the live negatives named. It does not ship a second dummy.

Same shape as last time — finish the last step:

OR

in disguise.ttlMs

, cacheScope

), a presence check is “a claim was made.” Do not call it verified until something can fail.If you cannot say which clause is still live, you do not have a negative case. You have a dummy that is wrong in a pile.

A README cannot lie to a test that reads the wire. A stamp can still lie to a README.

ttlMs

on the list is a reuse claim. contrast-smoke

now says which teaching clauses are still live. That is the list. Not the cache.

Claim = wire. Ask what you actually falsified.

Which clause on your tools/list is only present — not proven?

I'm an AAIF Ambassador. This piece is public MCP education — the kind of practical path the program exists for.

── more in #developer-tools 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/your-tools-list-is-s…] indexed:0 read:5min 2026-08-20 ·