{"slug": "your-tools-list-is-stamped-that-is-not-the-same-as-cached", "title": "Your tools/list is stamped. That is not the same as cached.", "summary": "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.", "body_md": "A stamp on `tools/list`\n\nis a claim about later reuse. Presence is not proof the next call is cacheable.\n\n**TL;DR** — The 7/28 spec put `ttlMs`\n\nand `cacheScope`\n\non `tools/list`\n\n. 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.\n\nThe 2026-07-28 spec added list-cache stamps (SEP-2549), modeled on HTTP `Cache-Control`\n\n. The official sentence is some version of: *clients know exactly how long tools/list is fresh.*\n\nThat sentence is doing a lot of work.\n\nHTTP already taught this. `Cache-Control: max-age=60`\n\nis a statement. It is not proof the next GET returns the same bytes. MCP imported the words. It did not import a test.\n\nA server can print `ttlMs: 60000, cacheScope: Public`\n\nand change its catalog on the next call. The stamp is still well-formed. A probe that only checks *presence* will still pass.\n\nLast piece: a labeled companion, `mcp-worse`\n\n, and one command — `contrast-smoke`\n\n— that passes only if the good server meets the BETTER list contract **and** the bad one fails it.\n\nThe checker looked like this:\n\n``` php\nfn is_lying_surface(p: &ListProbe) -> bool {\n    let unstamped = p.ttl_ms.is_none() || p.cache_scope.is_none();\n    let wrong_order = p.names != better_names();\n    unstamped || wrong_order\n}\n```\n\nThat `OR`\n\nis the hole.\n\n`mcp-better`\n\ngrew a third tool, `confirm_echo`\n\n. `mcp-worse`\n\nstill lists two: `echo`\n\n, `health`\n\n. So `wrong_order`\n\n(really: names ≠ the good catalog) is **always true**. The stamp half stopped carrying observable weight.\n\nIf worse grew `with_ttl_ms`\n\nand `with_cache_scope`\n\ntomorrow, the example would still print OK. It would fail for contents only. The negative case for `ttlMs`\n\nwould be gone, and nothing would say so.\n\nThe “companion must stay a reliable liar” guard only fires when **every** clause goes green at once. Partial decay is invisible.\n\nA comment on that post named it. They were right.\n\nA 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`\n\nthat hid which ones were still live.\n\nOrder is a property of the list the probe is **holding**. You can see it.\n\n`ttlMs`\n\nand `cacheScope`\n\nare 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`\n\n.\n\n`order-restart-smoke`\n\ndoes the right thing for **order**: two processes, same names. It applies that shape to TTL as `ttl_a == ttl_b`\n\n— the stamp is restart-stable. That is a property of the **number**, not of the caching behavior the number describes.\n\nFalsifying 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`\n\nis the first stamp with room to lie — and it is the clause with the least behind it.\n\nThis lab reads **this** list. Not the next call. Not a client cache.\n\nThe probe now requires **each** teaching clause on the companion. Stamp decay fails closed and names the clause.\n\n``` php\n// examples/contrast_smoke.rs — current tree\nfn is_unstamped(p: &ListProbe) -> bool {\n    p.ttl_ms.is_none() || p.cache_scope.is_none()\n}\n\nfn wrong_names(p: &ListProbe) -> bool {\n    p.names != better_names()\n}\n```\n\nBoth must stay true on `mcp-worse`\n\n. If either goes green, the example exits non-zero and says which.\n\n```\ngit clone https://github.com/Wolfe-Jam/mcp-better.git\ncd mcp-better\ncargo build --bins\n```\n\nThis is the **tree**, not `cargo install mcp-better`\n\n. The published `v0.5.0`\n\ntag still has the `OR`\n\n. The named-clause OK line is the honesty cut on current `main`\n\n.\n\n```\ncargo run --example contrast-smoke\n```\n\nExpect (captured 2026-08-19, current tree):\n\n```\nbetter names=[\"health\", \"echo\", \"confirm_echo\"] ttl=Some(60000) scope=Some(Public)\nworse  names=[\"echo\", \"health\"]                 ttl=None        scope=None\ncontrast-smoke: OK (better contract · worse unstamped · worse names≠health,echo,confirm_echo)\n```\n\nThe OK line is the point. It names which negatives are still live.\n\n| Claim | Evidence | Not proved |\n|---|---|---|\n| Good list is ordered + stamped | Wire: three names, `ttlMs > 0` , `cacheScope == Public`\n|\nThat a client should cache it |\n| Companion stays unstamped |\n`unstamped` asserted, not `OR` -ed away |\nCache behavior |\n| Companion catalog ≠ better |\n`names ≠ health,echo,confirm_echo` (today: two tools, reversed) |\nThat “wrong names” is only order |\n| Partial decay is visible | Stamp clause going green fails the example | A second mutant (stamped-but-reversed) |\n\nThat 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.\n\nSame shape as last time — finish the last step:\n\n`OR`\n\nin disguise.`ttlMs`\n\n, `cacheScope`\n\n), 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.\n\nA README cannot lie to a test that reads the wire. A stamp can still lie to a README.\n\n`ttlMs`\n\non the list is a reuse claim. `contrast-smoke`\n\nnow says which teaching clauses are still live. That is the list. Not the cache.\n\nClaim = wire. Ask what you actually falsified.\n\n**Which clause on your tools/list is only present — not proven?**\n\n*I'm an AAIF Ambassador. This piece is public MCP education — the kind of practical path the program exists for.*", "url": "https://wpnews.pro/news/your-tools-list-is-stamped-that-is-not-the-same-as-cached", "canonical_source": "https://dev.to/wolfejam/your-toolslist-is-stamped-that-is-not-the-same-as-cached-2j0f", "published_at": "2026-08-20 11:00:00+00:00", "updated_at": "2026-08-20 11:15:40.479882+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["MCP", "mcp-better", "mcp-worse", "Wolfe-Jam", "SEP-2549"], "alternates": {"html": "https://wpnews.pro/news/your-tools-list-is-stamped-that-is-not-the-same-as-cached", "markdown": "https://wpnews.pro/news/your-tools-list-is-stamped-that-is-not-the-same-as-cached.md", "text": "https://wpnews.pro/news/your-tools-list-is-stamped-that-is-not-the-same-as-cached.txt", "jsonld": "https://wpnews.pro/news/your-tools-list-is-stamped-that-is-not-the-same-as-cached.jsonld"}}