跳轉至

快取提示

機器翻譯

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

在 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:
    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")},
)

處理函式指定了 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(立刻過期),所以什麼都沒宣告的伺服器,看到的流量和以往一模一樣,一次呼叫就一次請求。

client.py
from dataclasses import dataclass
from typing import Any

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


@dataclass
class DemoState:
    fetches: int = 0
    now: float = 1_000_000.0


state = DemoState()


async def list_tools(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListToolsResult:
    state.fetches += 1
    return ListToolsResult(tools=[Tool(name="forecast", input_schema={"type": "object"})])


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


async def main() -> None:
    start = state.fetches
    async with Client(server, cache=CacheConfig(clock=lambda: state.now)) as client:
        await client.list_tools()  # fetch 1
        await client.list_tools()  # fresh for 60s: served from the cache
        state.now += 60.0
        await client.list_tools()  # the TTL ran out: fetch 2
        await client.list_tools(cache_mode="refresh")  # skip the cache read: fetch 3
        print(f"4 calls, {state.fetches - start} fetches")

四次呼叫,三次抓取。第二次呼叫找到新鮮的項目,根本沒送到伺服器;把(注入的)時鐘撥過 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(server, 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 則會完全關掉它。