mcp: ClientSession.ReadResource keeps uncacheable and expired results until the session ends
维护者通常 1 天内回复
还没有人认领这个 Issue。
- #1527 来自 @Assertive-xxh —— 已关闭,未合并
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 74/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- go
- 领域
- backend, performance
调研方向
Start in the mcp package by locating ClientSession.ReadResource, readResourceCache.put, and the cache eviction logic, then find the corresponding cache tests. Run the issue's in-memory transport reproduction to confirm heap growth with zero-TTL resources. Done means zero-TTL results are not retained, expired entries are bounded without affecting the list caches, and tests cover the behavior.
由索引模型根据 Issue 内容生成。
描述
Checklist
- I searched the existing issues, and this has not been reported before.
- This is a real problem that I ran into myself, using the latest release of the SDK.
- I ran into it in a production system, not in a toy example, a benchmark, or an automated or AI-driven scan of the code.
- I (a human) examined the relevant SDK code, and wrote or reviewed every line of this report. It is not unreviewed AI output.
- This report is brief and concise, and sticks to the facts needed to understand the problem.
What happened?
On a 2026-07-28 session, ClientSession.ReadResource stores each result in the session's resource cache whatever its ttlMs, and in most cases keeps it until the session ends. A client that reads many distinct URIs, for example through a resource template, holds all of their contents in memory. In the program below, a client reads 4000 distinct 64 KiB resources from a server that sets no TTL. On a 2026-07-28 session the heap grows by about 70 MiB per 1000 reads; on a 2025-11-25 session it stays at 2 MiB.
ReadResource calls readResourceCache.put without checking ttlMs, so it also stores results with ttlMs 0, which get never returns. A Go SDK server sends 0 unless a handler or ServerOptions.SetCacheable sets a TTL. An entry is removed only when its URI is read again after it expired, or when the server sends notifications/resources/updated for it, so results with a positive ttlMs also stay after they expire if their URI is not read again. Nothing reads expired entries: a failed read returns the error rather than a stale result.
Expected: a result with ttlMs 0 is not retained (the caching specification says it "SHOULD be considered immediately stale"), and expired entries do not accumulate for the life of the session. For comparison, the Python SDK does not store results with ttlMs 0, and the Python and TypeScript SDKs bound their default in-memory caches (to 1024 entries and to 512 resources/read entries).
If you agree, I'd like to fix this myself. My approach: ReadResource stores a result only when its ttlMs is positive, and the resource cache removes its expired entries each time it has doubled in size, so the cost is amortized over the reads. The list caches stay as they are, because lookupTool relies on stale tools/list entries (#1053). I have this working locally with tests, and I'm happy to go a different way if you prefer, for example a fixed cap like the TypeScript SDK's.
AI disclosure: I found this while reading the client cache code with Claude Code, which also helped write the reproduction and this report. I ran the reproduction and reviewed the code and the report myself.
How to reproduce
package main
import (
"context"
"fmt"
"log"
"runtime"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
ctx := context.Background()
// Every resource is 64 KiB of text. The server sets no TTL.
body := strings.Repeat("x", 64<<10)
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
server.AddResourceTemplate(&mcp.ResourceTemplate{Name: "doc", URITemplate: "mem://docs/{id}"},
func(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{Contents: []*mcp.ResourceContents{{URI: req.Params.URI, Text: body}}}, nil
})
for _, version := range []string{"2025-11-25", "2026-07-28"} {
t1, t2 := mcp.NewInMemoryTransports()
if _, err := server.Connect(ctx, t1, nil); err != nil {
log.Fatal(err)
}
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
cs, err := client.Connect(ctx, t2, &mcp.ClientSessionOptions{ProtocolVersion: version})
if err != nil {
log.Fatal(err)
}
for i := 1; i <= 4000; i++ {
res, err := cs.ReadResource(ctx, &mcp.ReadResourceParams{URI: fmt.Sprintf("mem://docs/%d", i)})
if err != nil {
log.Fatal(err)
}
if i%1000 == 0 {
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%s: %d resources read (ttlMs %d), heap in use %d MiB\n",
cs.InitializeResult().ProtocolVersion, i, res.TTLMs, m.HeapInuse>>20)
}
}
cs.Close()
}
}
// Sample output with v1.8.0 (the figures vary slightly between runs):
//
// 2025-11-25: 1000 resources read (ttlMs 0), heap in use 2 MiB
// 2025-11-25: 2000 resources read (ttlMs 0), heap in use 2 MiB
// 2025-11-25: 3000 resources read (ttlMs 0), heap in use 2 MiB
// 2025-11-25: 4000 resources read (ttlMs 0), heap in use 2 MiB
// 2026-07-28: 1000 resources read (ttlMs 0), heap in use 71 MiB
// 2026-07-28: 2000 resources read (ttlMs 0), heap in use 140 MiB
// 2026-07-28: 3000 resources read (ttlMs 0), heap in use 209 MiB
// 2026-07-28: 4000 resources read (ttlMs 0), heap in use 278 MiB
Where did you hit this?
Not in a production system; see the disclosure above.
Go MCP SDK version
v1.8.0 (also main at 0fa5e84)
Go version
go version go1.25.0 windows/amd64
Logs
No response
- 主要语言
- Go
- 星标
- 5.3k
- 派生
- 614
- 平均合并
- 23 小时 55 分钟
- 30 天内合并 PR
- 62
环境准备
在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
modelcontextprotocol/go-sdk 的其他 Issue
-
mcp: typed tool handlers lose wrapped JSON-RPC errors可能重新可做 关联的 PR 已关闭且未合并。 未关闭
难度 2/5 1-3 小时 新手友好度 85/100
modelcontextprotocol/go-sdk#1529 · 1 条评论 ·
维护者通常 1 天内回复
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensions可能重新可做 关联的 PR 已关闭且未合并。 未关闭needs investigation
难度 2/5 1-3 小时 新手友好度 72/100
modelcontextprotocol/go-sdk#745 · 13 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
mcp: streamable client keeps the session ID from a rejected server/discover, so the fallback initialize fails可能已有人在做 @simon0191 于 1 天前认领。 未关闭
modelcontextprotocol/go-sdk#1522 · 1 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
auth: OAuth client rejects protected resource metadata when the MCP server URL has a query string可能已有人在做 @beiryu 于 2 天前认领。 未关闭
难度 3/5 1-2 天 新手友好度 74/100
modelcontextprotocol/go-sdk#1519 · 2 条评论 ·
维护者通常 1 天内回复
-
`ping` and `logging/setLevel` still sent for protocol 2026-07-28可能已有人在做 @nahapetyan-serob 于 1 天前认领。 未关闭
难度 3/5 1-2 天 新手友好度 65/100
modelcontextprotocol/go-sdk#1518 · 1 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
查看 modelcontextprotocol/go-sdk 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 62/100
-
evener-transcript-v2-upgrade tool still scans transcript files through bufio's 4 KiB default buffer未关闭
难度 1/5 1 小时以内 新手友好度 78/100
prime-radiant-inc/evener#6370 ·
维护者通常 1 天内回复
-
ManageSieve proxy: a backend's NO/BYE greeting is skipped as a capability line and reported as EOF未关闭bug
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复
-
Mend: dependency security vulnerability
难度 2/5 1-3 小时 新手友好度 65/100
-
Mend: dependency security vulnerability
难度 2/5 1-3 小时 新手友好度 62/100
ManageIQ/manageiq-pods#1476 ·
维护者通常 1 天内回复