MCP 2026-07-28 one lesson per page
23 lessons
Fails quietlyInteractions & Execution · 26%Track 2 · 7 / 14

Every result says what it is, and how long it stays fresh

Results must carry resultType. Lists, reads and discovery must also carry ttlMs and cacheScope, so clients and proxies know how long to cache them.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

Two thousand agents, one tool list

Your company gateway serves 2,000 employees' agents, and each calls tools/list at the start of every conversation. That's thousands of identical calls a minute hitting your servers.

With ttlMs: 300000 and cacheScope: "public", the gateway answers from cache. Your server sees one call every five minutes. Because the order is deterministic, the model's prompt prefix stays the same, so LLM prompt caching kicks in too.

The cautionary tale: a developer marks resources/read of app://me/payroll as "public". The gateway caches Alice's salary and serves it to Bob. cacheScope is a promise about the data, and the spec says plainly that it is not an access control.

resultType is required on every result: "complete", "input_required", or a value added by an extension the client advertised (Tasks adds "task"). If the field is absent, the client treats the result as "complete"; that's the rule for older servers. An unrecognised value is invalid.

Caching hints are required on complete results from server/discover, tools/list, prompts/list, resources/list, resources/templates/list and resources/read:

  • ttlMs: milliseconds the result may be treated as fresh. It must be ≥ 0, and 0 means stale immediately. A missing value is treated as 0, and a negative one is ignored and treated as 0.
  • cacheScope: "public" means no user-specific data, so any shared gateway may cache it. "private" means it can be reused only within the same authorization context.

input_required results and results from MRTR retries are never cached. A change notification immediately invalidates a fresh cache entry. Servers SHOULD return tools/list in a deterministic order.

Without a type tag, a client can't tell a final answer from "I need more input". Without cache hints, clients poll list endpoints blindly. Explicit TTLs, the same idea as HTTP Cache-Control, cut that traffic. A stable tool order also makes the model's prompt-cache hits more likely.

Message flow in 2026-07-28
ClientServertools/list{tools, ttlMs: 300000, public}Need tools again 2 min later: still fresh, use the cachenotifications/tools/list_changedInvalidate at once, even though the TTL hasn't expiredtools/list
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

A tool list

2026-07-28
{"jsonrpc": "2.0", "id": 3, "result": {  "resultType": "complete",  "tools": [ { "name": "get_weather", "inputSchema": { … } } ],  "ttlMs": 300000,  "cacheScope": "public"}}
What it used to look like (legacy, for comparison only)
Legacybefore
{"jsonrpc": "2.0", "id": 3, "result": {  "tools": [ { "name": "get_weather", "inputSchema": { … } } ]}}

A per-user resource must be private

2026-07-28
{"jsonrpc": "2.0", "id": 4, "result": {  "resultType": "complete",  "contents": [{ "uri": "app://me/settings", "mimeType": "application/json", "text": "{…}" }],  "ttlMs": 60000,  "cacheScope": "private"}}

cacheScope is a promise about the data, not an access control. Every page of a paginated list must use the same scope.

  • A result with no resultType from a modern server is non-compliant. Clients assume complete, which hides the mistake until you add MRTR.
  • List and read results without ttlMs/cacheScope are non-compliant. Worse, marking user-specific data "public" lets a shared gateway serve one user's data to another.
  • Treat the TTL as a freshness check when the data is needed, not as a polling timer. If you do poll, add jitter and backoff.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
# Every result has resultType; lists carry the cache hintsmcp tools/list '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{'"$META"'}}' | grep -o '"resultType":"[a-z_]*"\|"ttlMs":[0-9]*\|"cacheScope":"[a-z]*"' # Determinism: two calls should list tools in the same orderdiff <(mcp tools/list '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{'"$META"'}}') <(mcp tools/list '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{'"$META"'}}') # Review: any resources/read that depends on the caller must say "private"

Answer all 3 correctly and this lesson is marked as learned.

Q1resources/read returns the caller's own account settings. Which cacheScope?

Q2A client gets a result with no resultType field. How should it treat it?

Q3An older server returns a tools/list result with no ttlMs. How long should a modern client consider it fresh?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.