mcp: ReadResource caches the result of a multi round-trip retry and serves a retry request from the cache
メンテナーはふだん 1 日以内に返信
評価
調査の方向性
Start in ClientSession.ReadResource in mcp/client.go (around lines 1388-1407), where the cache lookup and store happen, and read how the multi round-trip middleware in mcp/mrtr.go (clientMultiRoundTripMiddleware, around line 64) retries inside handleSend. Skip the cache when the request carries InputResponses or RequestState, and do not store interim input_required results or retry results. Done when the two regression tests named in the report, TestMultiRoundTrip_ReadResource_RetryResultNotCached and TestMultiRoundTrip_ReadResource_ManualRetryBypassesCache in mrtr_test.go, pass and cacheable reads without input still hit the cache.
索引モデルが issue の本文から書いたものです。
説明
Describe the bug
On a 2026-07-28 session, ClientSession.ReadResource keeps a per-URI cache of resources/read results. It does not take multi round-trip requests (SEP-2322) into account, in two ways:
- The result that
ReadResourcestores is whateverhandleSendreturns, and the multi round-trip sending middleware runs insidehandleSend(mcp/mrtr.go:64,clientMultiRoundTripMiddleware). So the result of the retry that carriedinputResponsesandrequestStateis cached under the plain URI (mcp/client.go:1404on main 3c08147). A resource gated behind an elicitation is served from the cache on the next read without asking the user again. - The cache lookup happens before the request is sent and is keyed by URI only (
mcp/client.go:1394). With the middleware disabled, the caller's manual retry (same URI, now withInputResponsesandRequestState) is answered from the cache with the interiminput_requiredresult from the first read. The retry never reaches the server. The interim result itself should not have been cached either.
To Reproduce
package main
import (
"context"
"fmt"
"log"
"sync/atomic"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
ctx := context.Background()
var handlerCalls, elicitations atomic.Int32
server := mcp.NewServer(&mcp.Implementation{Name: "s", Version: "0"}, &mcp.ServerOptions{
SetCacheable: func(_ context.Context, _ mcp.Request, c *mcp.Cacheable) {
c.TTLMs = 60_000
c.CacheScope = "private"
},
})
server.AddResource(&mcp.Resource{URI: "secret://doc", Name: "doc"},
func(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
handlerCalls.Add(1)
if req.Params.InputResponses == nil {
return &mcp.ReadResourceResult{
InputRequests: mcp.InputRequestMap{"confirm": &mcp.ElicitParams{Message: "Reveal the document?"}},
RequestState: "state-1",
}, nil
}
return &mcp.ReadResourceResult{Contents: []*mcp.ResourceContents{{URI: req.Params.URI, Text: "the document"}}}, nil
})
ct, st := mcp.NewInMemoryTransports()
if _, err := server.Connect(ctx, st, nil); err != nil {
log.Fatal(err)
}
client := mcp.NewClient(&mcp.Implementation{Name: "c", Version: "0"}, &mcp.ClientOptions{
ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
elicitations.Add(1)
return &mcp.ElicitResult{Action: "accept"}, nil
},
})
cs, err := client.Connect(ctx, ct, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
for i := 1; i <= 2; i++ {
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: "secret://doc"})
if err != nil {
log.Fatal(err)
}
fmt.Printf("read %d: contents=%d handlerCalls=%d elicitations=%d ttlMs=%d\n",
i, len(res.Contents), handlerCalls.Load(), elicitations.Load(), res.TTLMs)
}
}
Output on v1.8.0 and on main (3c08147):
read 1: contents=1 handlerCalls=2 elicitations=1 ttlMs=60000
read 2: contents=1 handlerCalls=2 elicitations=1 ttlMs=60000
The second read never reaches the server and never asks the user. It returns the same *ReadResourceResult pointer as the first read.
For the second symptom, create the client with MultiRoundTrip: &mcp.MultiRoundTripOptions{Disabled: true}, read once, then read again with InputResponses and the returned RequestState. The second call returns the first call's interim result (NeedsInput() is true, Contents is empty) and the handler is called once in total.
Expected behavior
- The second automatic read goes to the server again and asks the user again:
handlerCalls=4 elicitations=2. - A read that carries
InputResponsesorRequestStateis sent to the server and is not answered from the cache. - Neither an interim
input_requiredresult nor the result of a retry is stored in the cache. - A
resources/readresult that needed no input is still cached as today.
Logs
Regression tests from the fix, run against main before the fix:
--- FAIL: TestMultiRoundTrip_ReadResource_RetryResultNotCached (0.00s)
mrtr_test.go:1066: read 2: handler calls = 2, want 4
mrtr_test.go:1069: read 2: elicitations = 1, want 2; the retry result was served from the cache
--- FAIL: TestMultiRoundTrip_ReadResource_ManualRetryBypassesCache (0.00s)
mrtr_test.go:1116: retry: NeedsInput() = true, want false; the interim result was served from the cache
Additional context
Specification 2026-07-28, Caching, Cache Key: "Results produced by retrying a request through the multi round-trip requests mechanism, that is, requests carrying inputResponses or requestState, MUST NOT be cached, as they depend on inputs that are not part of the cache key." The same section: "Clients MUST NOT serve a cached response for a request whose method or parameters differ from the request that produced it." And under Cacheable Results: "Interim results with resultType: "input_required" are not cacheable and carry no caching hints."
https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cache-key
Root cause: mcp/client.go:1388-1407 (ClientSession.ReadResource) looks the URI up and stores the result without regard to InputResponses, RequestState, NeedsInput() or whether the middleware retried.
Related observation, not part of this report: on the server side, Server.readResource calls resolveCacheable before it returns an input_required result (mcp/server.go:1136), so interim results go out with ttlMs and cacheScope set, while the specification says they carry no caching hints.
SDK versions: v1.8.0 (3f3b699) and main at 3c0814798be7e813740406029e8efef07828f34f (2026-10-09).
Go version: go1.27.1 darwin/arm64.
I have a fix with regression tests ready and will open a pull request.
- 主要言語
- Go
- スター
- 5.3k
- フォーク
- 606
- 平均マージ
- 1日 8時間
- マージ済み PR(30日)
- 70
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
modelcontextprotocol/go-sdk のほかの issue
-
Session-lifecycle bookkeeping logs at info, spamming multiple log lines per request on stateless streamable HTTP対応中かも @anneheartrecord が 61 日前に担当しました。 オープンP3
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
modelcontextprotocol/go-sdk#1204 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensions対応中かも @ajuijas が 227 日前に担当しました。 オープンneeds investigation
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
modelcontextprotocol/go-sdk#745 · コメント 13 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
internal/json: Unmarshal allocates a 32 KiB stream buffer on every call対応中かも @raveesh-me が 2 日前に担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 25/100
modelcontextprotocol/go-sdk#1453 ·
メンテナーはふだん 1 日以内に返信
-
auth/extauth: ClientCredentialsHandler does not send the resource parameter対応中かも @shlee1223 が 3 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 25/100
modelcontextprotocol/go-sdk#1450 ·
メンテナーはふだん 1 日以内に返信
-
mcp: LoggingHandler drops records logged at custom slog levels対応中かも @kaankucukx が 4 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 45/100
modelcontextprotocol/go-sdk#1445 ·
メンテナーはふだん 1 日以内に返信
modelcontextprotocol/go-sdk の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
yetone/magpie#4095 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
docs: Google Safe Browsing flags go-bonnie.dev as "deceptive" (pipe-to-shell install instructions)オープンdocumentation
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 2 日以内に返信
-
[Bug]: Telegram bot (v3.11.0): "Ban log" no longer sends the current 3xipl-banned.log対応中かも @HamedSanaei が今日担当しました。 オープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
MHSanaei/3x-ui#6990 · コメント 1 件 ·
メンテナーはふだん 3 日以内に返信
-
bug(tool/internal/instrument): identical inject_code rules from two rule packages are applied twiceオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
open-telemetry/opentelemetry-go-compile-instrumentation#1611 ·
メンテナーはふだん 2 日以内に返信