跳轉至

快取提示

機器翻譯

本頁是從英文說明文件自動翻譯而來,以英文頁面為準。如果哪裡讀起來不對勁,翻譯有說明如何回報。

在 2026-07-28 協定上,伺服器為 tools/listprompts/listresources/listresources/templates/listresources/readserver/discover 回傳的每個結果都帶有兩個欄位:ttlMs,表示用戶端可以把這個結果視為新鮮的毫秒數;cacheScope,表示快取的結果可以跨使用者共用("public"),還是只屬於某一個授權上下文("private")。

伺服器本身什麼都不快取。這兩個欄位是一種宣告:「這份工具清單對所有人都一樣,而且一分鐘內不會變。」用戶端(或擋在你前面的閘道)就可以省掉這次往返。要不要遵守這些提示,由用戶端決定;送出這些提示則是伺服器的工作,而 SDK 會替你處理。

預設情況下,每個結果都是 ttlMs: 0, cacheScope: "private":立刻過期、永不共用。這永遠安全,也永遠符合規範。如果你的清單確實穩定,而且對所有呼叫端都相同,就在建構時說清楚:

server.py
from mcp.server import CacheHint, MCPServer

mcp = MCPServer(
    "Weather",
    cache_hints={
        "tools/list": CacheHint(ttl_ms=60_000, scope="public"),
        "resources/read": CacheHint(ttl_ms=5_000),
    },
)


@mcp.tool()
def forecast(city: str) -> str:
    return f"Sunny in {city}"


@mcp.resource("config://units")
def units() -> str:
    return "metric"
  • 這個對應表以方法名稱為鍵,而且只有這六個可快取的方法是合法的鍵。參數的型別是 Mapping[CacheableMethod, CacheHint],所以編輯器會自動完成這些鍵,並在執行前標出拼字錯誤;任何躲過型別檢查器的錯誤,都會在建構時引發例外。
  • 沒提到的方法就維持預設值。這個對應表是一組覆寫,不是完整清單。
  • CacheHint(ttl_ms=5_000) 沒有設定 scope,所以維持 "private":每個呼叫端各自享有五秒的新鮮期。範圍和 TTL 是兩個各自獨立的決定。
  • "server/discover" 也是合法的鍵,因為探索結果和任何清單一樣可以快取。

Warning

cacheScope: "public" 的意思是任何人都可能收到你快取的回應。共用的閘道會毫不猶豫地把某個使用者的結果交給另一個使用者,即使請求經過身分驗證也一樣。只有在結果對每個呼叫端都完全相同時,才把它標成 "public";也絕對不要把 cacheScope 當成存取控制:它是標籤,不是鎖。

個別處理函式的覆寫

在低階的 Server 上,處理函式自己手動組出結果,而 ttl_ms / cache_scope 只是結果模型上的欄位。明確設定這些欄位的處理函式,永遠勝過建構子的對應表,而且是逐欄位比較:

server.py
from typing import Any

from mcp.server import CacheHint, Server, ServerRequestContext
from mcp.types import ListToolsResult, PaginatedRequestParams, Tool

TOOLS = [Tool(name="forecast", input_schema={"type": "object"})]


async def list_tools(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListToolsResult:
    print("tools/list served")
    return ListToolsResult(tools=TOOLS, ttl_ms=1_000)


server = Server(
    "Weather",
    on_list_tools=list_tools,
    cache_hints={"tools/list": CacheHint(ttl_ms=60_000, scope="public")},
)
app = server.streamable_http_app()

處理函式指定了 ttl_ms=1_000,但對範圍隻字未提。線路上的結果是:ttlMs: 1000(來自處理函式,不是對應表的 60_000)和 cacheScope: "public"(來自對應表,因為處理函式沒設定)。明確指定的勝過建構時設定的,建構時設定的又勝過預設值。這條規則是逐欄位套用的,所以處理函式可以釘住一個欄位,把另一個欄位交給全伺服器的政策。

這也是應付建構子無從得知的動態情況的出口:一個依使用者過濾 resources/read 的處理函式,可以在其他部分都是 public 的伺服器上,針對某一個 URI 回傳 cache_scope="private"

分頁清單有一點要注意:協定要求同一份清單的每一頁都要有相同的 cacheScope。建構子的對應表天生就滿足這一點,因為它以方法為鍵,而不是以頁為鍵。但自行覆寫範圍的處理函式,就得自己負責這份一致性:要在每一頁都覆寫,絕不能只在有 cursor 時才覆寫,否則第一頁和第二頁會對不上。

用戶端看到什麼

在 2026-07-28 的工作階段(session)上,Client 會替你遵守這些提示:它內建一個回應快取,預設開啟。帶著 ttlMs 抵達的結果會被存起來,在 TTL 內完全相同的呼叫會直接由快取提供,不需要往返。沒有帶提示的結果不會被快取:沒有提示的結果會套用 CacheConfig.default_ttl_ms,它預設為 0(立刻過期),所以什麼都沒宣告的伺服器,看到的流量和以往一模一樣,一次呼叫就一次請求。

要親眼看看這個過程,就用 uvicorn 提供前一節的 server.py(它的最後一行會建立 ASGI 應用程式)。處理函式每次真正執行時都會印出一行:

uvicorn server:app --port 8000
client.py
from dataclasses import dataclass

import anyio

from mcp import Client
from mcp.client import CacheConfig
from mcp.types import ListToolsResult


@dataclass
class Clock:
    now: float = 0.0


clock = Clock()  # advanced by hand below, so the TTL runs out without sleeping


async def run(client: Client) -> ListToolsResult:
    tools = await client.list_tools()  # fetch 1
    await client.list_tools()  # still fresh: served from the cache
    clock.now += 2
    await client.list_tools()  # past the one-second TTL: fetch 2
    await client.list_tools(cache_mode="refresh")  # skip the cache read: fetch 3
    return tools


async def main() -> None:
    async with Client("http://localhost:8000/mcp", cache=CacheConfig(clock=lambda: clock.now)) as client:
        tools = await run(client)
        print(tools.ttl_ms, tools.cache_scope)


if __name__ == "__main__":
    anyio.run(main)

在第二個終端機執行 python client.py。它會印出第一個結果帶著的提示,處理函式的 ttlMs 和對應表的 cacheScope 並排:

1000 public

伺服器的終端機交代了剩下的部分:在 uvicorn 的請求記錄之間,tools/list served 出現了三次。

四次呼叫,三次抓取。第二次呼叫找到新鮮的項目,根本沒送到伺服器;把(注入的)時鐘撥過 TTL 之後,第三次又重新抓取;第四次則指定了 cache_mode="refresh"。這個關鍵字引數存在於五個會快取的動詞上(list_toolslist_promptslist_resourceslist_resource_templatesread_resource):

  • "use"(預設)如果有新鮮的項目就直接提供,沒有的話就抓取並存起來。
  • "refresh" 從不由快取提供:它會抓取並儲存結果,取代原本快取的內容。
  • "bypass" 直接往返,完全不碰快取:不讀、也不寫。

有一條規則凌駕於 "use" 之上:帶有 meta 的呼叫一定會送到伺服器。設定了 meta 的請求(進度 token、追蹤欄位)期待的是一個實際送上線路的請求,所以在 cache_mode="use" 下會被當成 "refresh" 處理:跳過快取讀取,而抓取回來的結果仍然會取代快取中的項目。"bypass" 和明確指定的 "refresh" 行為照舊。

要完全關掉快取,就在建構 Client 時傳入 cache=None:每次呼叫又都變回一次往返,而 cache_mode 雖然仍可接受,但不會有任何作用。

範圍也會自動遵守:"private" 項目綁定在快取的分區(partition)上(見下文),而 "public" 項目則可以選擇更廣的共用。此外,對通知點名的那些項目來說,通知勝過 TTLlist_changed 通知會逐出對應的快取清單,resources/updated 則會逐出恰好存在該 URI 下的快取讀取結果,不管它們有多新鮮。在 2026-07-28 連線上,這些通知是透過你用 client.listen(...) 開啟的 subscriptions/listen 串流送達的,而且逐出會在你的監看程式看到事件之前完成;詳情請見 訂閱

resources/updated 有一點要注意:逐出只比對完全相同的 URI。存放區的契約沒有列舉或掃描的操作(和參考的 TypeScript 實作一樣),所以帶著資源 URI 的通知不會逐出其父資源的快取讀取結果。如果你的伺服器是用這種方式通知子資源的變動,就用 cache_mode="refresh" 重新抓取父資源。

設定方式:CacheConfig

from mcp.client import CacheConfig

client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
  • store:項目存放的地方。預設是每個用戶端各自一個全新的記憶體內存放區;傳入你自己的 ResponseCacheStore 實作(例如以 Redis 為後端)就能跨用戶端或跨處理程序共用快取。契約型別(ResponseCacheStoreCacheKeyCacheEntry,以及預設的 InMemoryResponseCacheStore)都可以從 mcp.client 匯入。一次查詢最多可能對存放區連續發出兩次 get(先查 private 分支,再查 public 分支),所以遠端存放區的延遲預期要據此估算。自訂存放區必須搭配明確的 partition
  • partition:授權上下文的標籤,用來避免在共用存放區中把某個主體的 "private" 項目提供給另一個主體。
  • target_id:明確的伺服器身分,用於自訂傳輸和同處理程序內的伺服器(見下文)。
  • default_ttl_ms:套用在沒有帶 ttlMs 提示的結果上的 TTL。預設的 0 讓沒有提示的結果不被快取。
  • share_public:跨分區提供伺服器宣稱為 "public" 的項目(見下文)。預設關閉。
  • clock:牆上時鐘的來源,以 epoch 秒為單位。像上面的範例那樣注入一個,過期測試就不需要 sleep。

分區 = 經過驗證的主體

partition 要從經過驗證的憑證推導出來,例如已驗證權杖的 subject。絕不要從請求提供的資料推導,也絕不要從伺服器 URL 推導(伺服器身分是另一條獨立的鍵軸)。SDK 是一個函式庫,本身沒有任何身分驗證:信任的錨點是建構 CacheConfig 的人,也就是部署方,而不是租戶。多租戶閘道要為每個已驗證的主體各建立一個 CacheConfig

分區在 Client 的整個存活期間也是固定的。如果連線的授權上下文在工作階段中途改變(例如重新驗證成另一個主體),快取不會跟著變;請為新的主體建構一個新的 Client

快取鍵也帶有伺服器的身分:你連線的 URL 字串,去掉任何 user:pass@ 使用者資訊,其餘逐位元組保留。不做大小寫摺疊、不重排查詢參數、不清理結尾斜線。正規化不足只會損失共用的機會,過度正規化卻可能把兩個租戶合併在一起(?tenant=a?tenant=b),所以表面上不同的 URL 就是不共用項目。沒有 URL 的時候(同處理程序內的伺服器,或 Transport 實例),用戶端會改拿到一個每個實例隨機產生的身分;設定 CacheConfig.target_id 來替伺服器命名(使用自訂存放區時這是必要的,建構時也會這麼告訴你)。身分在進入鍵的材料之前會先經過 sha256 雜湊,所以查詢字串裡帶著機密的 URL 永遠不會出現在存放區的鍵中。你自己也不要把雜湊前的形式記錄下來。

share_public 代表信任伺服器,而且是整個機群一起信任

預設情況下,即使是 "public" 項目也會留在自己的分區內。share_public=True 會把伺服器標成 cacheScope: "public" 的項目提供給使用該存放區的每一個分區,等於代替它們全體信任伺服器的分類。如果伺服器(因為 bug 或惡意)把 "public" 蓋在各租戶專屬的資料上,某個租戶的回應就會洩漏給其他租戶。這個旗標刻意只放在建構子層級:每次呼叫的 cache_mode 可以縮小快取範圍,但沒有任何每次呼叫層級的東西可以擴大共用。

快取絕不會做的事

  • 工作階段層級的呼叫會繞過它。 client.session.list_tools() 這一類的呼叫一定會往返;快取是掛在 Client 的動詞上。
  • server/discover 不參與。 discover 結果只在連線時送達一次,永遠不會進入回應快取,即使它帶著 ttlMs 也一樣。如果你自己把它保存下來以跳過重新連線時的探測(prior_discover),它的新鮮度就由你自己記帳:DiscoverResult 帶有已解析好的 ttl_mscache_scope,正是為了這個用途。
  • 後續分頁永遠不會被快取。 只有不帶 cursor 的呼叫會參與。因為 cursor 過期而被拒絕的後續分頁倒是會逐出快取的清單,因為清單已經在底下變了。
  • 多輪往返(multi-round-trip)的讀取永遠不會被快取。input_responses/request_state 起頭的 read_resource,或是經過輸入回合才解析出結果的讀取,都永遠不會進入快取(這是規範的 MUST)。
  • 靠通知逐出,就得有通知。 逐出的效果取決於傳輸能不能把通知送到,而現代的同處理程序內路徑(Client(server) 搭配預設的 mode="auto")目前不會遞送獨立的通知。
  • 逐出是最終發生,不是立即發生。 走線路的通知是從衍生出來的 task 分派的,所以和通知抵達搶時間的呼叫,可能會再被提供一次逐出前的項目;這個空窗受分派延遲所限,而逐出終究會生效。
  • 沒有 stale-if-error。 過期的項目絕不會因為重新抓取失敗就被拿出來提供;錯誤會往上傳遞。
  • 沒有提前重新抓取。 已存的項目會一直提供到 TTL 過期為止,過期後的下一次呼叫要付出往返的代價;背景不會有任何東西在更新。
  • 沒有合併。 兩個同時發出的相同呼叫就是兩次抓取。
  • TTL 不會超過 24 小時。 更大的 ttlMs,不論是伺服器送來的還是設定的,在存入時都會被壓到上限(mcp.client.caching.MAX_TTL_MS),這限制了任何項目能被提供的時間,不管它的提示有多大方。
  • 共用存放區上,用戶端之間會互相競爭。當逐出搶在進行中的抓取之前發生時,每個用戶端會丟棄自己的寫入,但共用同一存放區的其他用戶端仍然可能把一個項目寫回去,而那個項目其實已經被一次它沒看到的逐出移除了;這份競爭的記帳本身也有上限:追蹤的鍵超過 4096 個時,最舊那個鍵的防護會先被丟掉。這兩個空窗都是可接受的,並且由上面的 TTL 上限收尾。
  • 不會跨協定世代提供。 項目的範圍限定在協商出來的協定版本:在共用的持久性存放區上,工作階段絕不會提供在另一個協商版本下寫入的項目(同一份清單在不同世代確實不一樣,因為 SDK 會替較舊的工作階段剝掉 2026 的欄位)。逐出同樣只碰目前世代的項目;其他世代的項目就靠 TTL 自然老化淘汰。

自己讀取提示

這些提示也是每個可快取結果上的普通欄位(result.ttl_msresult.cache_scope,已解析好),如果你想在內建快取之上(或取而代之)疊上自己的記帳機制,可以直接用。

面對較舊的伺服器(2026 之前的協定),這些欄位在線路上根本不存在,模型會顯示保守的預設值:ttl_ms == 0cache_scope == "private",過期且不共用,對一個什麼都沒宣告的伺服器來說是正確的假設。快取對待舊版工作階段的方式也一樣:在那裡永遠不參考提示(不管線路上出現什麼鍵),只套用 default_ttl_ms,而它的預設值 0 什麼都不快取,所以 2026 之前的連線行為和快取存在之前一模一樣。如果需要區分「伺服器說了 0」和「伺服器什麼都沒說」,就檢查 "ttl_ms" in result.model_fields_set:只有欄位真的送達時它才會被設定。

較舊的用戶端

使用 2026 之前協定版本的用戶端永遠看不到這兩個欄位;SDK 在為這些連線序列化時就把它們剝掉了。提示只要設定一次;沒有任何需要針對版本另外寫的東西。

重點回顧

  • 六個方法帶有 ttlMs/cacheScope;SDK 把它們預設為 0/"private",過期且不共用,永遠安全。
  • 建構時的 cache_hints={method: CacheHint(...)}MCPServerServer 都有)會為每個方法設定全伺服器的值。
  • 在結果上設定這些欄位的處理函式會逐欄位覆寫對應表。
  • "public" 是一個承諾:結果對每個呼叫端都完全相同。它不是存取控制。
  • Client 會自動遵守提示:它的回應快取預設開啟,會提供新鮮的項目而不重新抓取,而對沒有提供提示的伺服器(或工作階段)則什麼都不快取。
  • 每次呼叫可用 cache_mode="refresh" 重新抓取、用 "bypass" 跳過快取;建構時傳入 cache=None 則會完全關掉它。