diff --git a/mcp/cache.go b/mcp/cache.go new file mode 100644 index 00000000..3db23e75 --- /dev/null +++ b/mcp/cache.go @@ -0,0 +1,95 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by the license +// that can be found in the LICENSE file. + +package mcp + +import ( + "sync" + "time" +) + +// methodCache is a per-method TTL cache for list and read results, as +// described in SEP-2549. Each entry is keyed by cursor (for paginated list +// methods) or URI (for resources/read). +type methodCache[R CacheableResult] struct { + mu sync.Mutex + cachedValues map[string]*cacheEntry[R] +} + +type cacheEntry[R CacheableResult] struct { + result R + receivedAt time.Time +} + +func (e *cacheEntry[R]) isValid() bool { + return time.Since(e.receivedAt) < time.Duration(e.result.GetTTLMs())*time.Millisecond +} + +func (mc *methodCache[R]) get(key string) (R, bool) { + mc.mu.Lock() + defer mc.mu.Unlock() + entry, ok := mc.cachedValues[key] + if !ok { + var zero R + return zero, false + } + if entry.result.GetTTLMs() <= 0 || !entry.isValid() { + delete(mc.cachedValues, key) + var zero R + return zero, false + } + return entry.result, true +} + +func (mc *methodCache[R]) put(key string, result R) { + mc.mu.Lock() + defer mc.mu.Unlock() + if mc.cachedValues == nil { + mc.cachedValues = make(map[string]*cacheEntry[R]) + } + mc.cachedValues[key] = &cacheEntry[R]{ + result: result, + receivedAt: time.Now(), + } +} + +func (mc *methodCache[R]) forEach(f func(R)) { + mc.mu.Lock() + defer mc.mu.Unlock() + for _, entry := range mc.cachedValues { + f(entry.result) + } +} + +func (mc *methodCache[R]) invalidate() { + mc.mu.Lock() + defer mc.mu.Unlock() + clear(mc.cachedValues) +} + +func (mc *methodCache[R]) invalidateKey(key string) { + mc.mu.Lock() + defer mc.mu.Unlock() + delete(mc.cachedValues, key) +} + +// cursorParams is the constraint for list-method params that carry a pagination +// cursor and can be checked for nil. Both methods are already implemented by +// every concrete list-params type. +type cursorParams interface { + Params + cursorPtr() *string +} + +// cachedListResult returns a cached list result keyed by the request cursor +// (SEP-2549). It returns the zero value and false on miss or when params is nil. +func cachedListResult[P cursorParams, R CacheableResult](cache *methodCache[R], params P) (R, bool) { + key := "" + if !params.isNil() { + if cp := params.cursorPtr(); cp != nil { + key = *cp + } + } + return cache.get(key) +} diff --git a/mcp/client.go b/mcp/client.go index 1e0e18a4..d73ac101 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -422,16 +422,16 @@ type ClientSession struct { // only set synchronously during Client.Connect. state clientSessionState + // Per-method TTL caches for list results (SEP-2549). + toolsCache methodCache[*ListToolsResult] + promptsCache methodCache[*ListPromptsResult] + resourcesCache methodCache[*ListResourcesResult] + resourceTemplatesCache methodCache[*ListResourceTemplatesResult] + readResourceCache methodCache[*ReadResourceResult] + // Pending URL elicitations waiting for completion notifications. pendingElicitationsMu sync.Mutex pendingElicitations map[string]chan struct{} - - // toolCacheMu guards toolCache. - toolCacheMu sync.RWMutex - // toolCache stores tool definitions keyed by name. - // It is used to look up x-mcp-header annotations when - // constructing Mcp-Param-* headers for tools/call requests. - toolCache map[string]*Tool } type clientSessionState struct { @@ -513,19 +513,25 @@ func (cs *ClientSession) Wait() error { return cs.conn.Wait() } -func (cs *ClientSession) cacheTools(tools []*Tool) { - cs.toolCacheMu.Lock() - defer cs.toolCacheMu.Unlock() - cs.toolCache = make(map[string]*Tool, len(tools)) - for _, tool := range tools { - cs.toolCache[tool.Name] = tool - } -} - -func (cs *ClientSession) getCachedTool(name string) *Tool { - cs.toolCacheMu.RLock() - defer cs.toolCacheMu.RUnlock() - return cs.toolCache[name] +// lookupTool returns the most recently seen definition of the tool with the +// given name across all cached ListTools results, or nil if no such tool has +// been seen. It is used by CallTool to inject the tool definition into the +// outgoing request context for transport-layer features (e.g. x-mcp-header +// param annotations). +func (cs *ClientSession) lookupTool(name string) *Tool { + var found *Tool + cs.toolsCache.forEach(func(r *ListToolsResult) { + if found != nil { + return + } + for _, t := range r.Tools { + if t.Name == name { + found = t + return + } + } + }) + return found } // registerElicitationWaiter registers a waiter for an elicitation complete @@ -1135,11 +1141,24 @@ func (cs *ClientSession) Ping(ctx context.Context, params *PingParams) error { } // ListPrompts lists prompts that are currently available on the server. +// +// Results may be served from a client-side TTL cache populated by previous +// calls; see SEP-2549. func (cs *ClientSession) ListPrompts(ctx context.Context, params *ListPromptsParams) (*ListPromptsResult, error) { if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.promptsCache, params); ok { + return result, nil + } params = injectRequestMeta(cs, params) } - return handleSend[*ListPromptsResult](ctx, methodListPrompts, newClientRequest(cs, orZero[Params](params))) + result, err := handleSend[*ListPromptsResult](ctx, methodListPrompts, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.promptsCache.put(params.Cursor, result) + } + return result, nil } // GetPrompt gets a prompt from the server. @@ -1153,6 +1172,9 @@ func (cs *ClientSession) GetPrompt(ctx context.Context, params *GetPromptParams) // ListTools lists tools that are currently available on the server. func (cs *ClientSession) ListTools(ctx context.Context, params *ListToolsParams) (*ListToolsResult, error) { if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.toolsCache, params); ok { + return result, nil + } params = injectRequestMeta(cs, params) } result, err := handleSend[*ListToolsResult](ctx, methodListTools, newClientRequest(cs, orZero[Params](params))) @@ -1160,7 +1182,9 @@ func (cs *ClientSession) ListTools(ctx context.Context, params *ListToolsParams) return nil, err } result.Tools = filterValidTools(cs.client.opts.Logger, result.Tools) - cs.cacheTools(result.Tools) + if cs.usesNewProtocol() { + cs.toolsCache.put(params.Cursor, result) + } return result, nil } @@ -1175,7 +1199,7 @@ func (cs *ClientSession) CallTool(ctx context.Context, params *CallToolParams) ( // Avoid sending nil over the wire. params.Arguments = map[string]any{} } - if tool := cs.getCachedTool(params.Name); tool != nil { + if tool := cs.lookupTool(params.Name); tool != nil { ctx = context.WithValue(ctx, toolContextKey, tool) } if cs.usesNewProtocol() { @@ -1192,25 +1216,59 @@ func (cs *ClientSession) SetLoggingLevel(ctx context.Context, params *SetLogging // ListResources lists the resources that are currently available on the server. func (cs *ClientSession) ListResources(ctx context.Context, params *ListResourcesParams) (*ListResourcesResult, error) { if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.resourcesCache, params); ok { + return result, nil + } params = injectRequestMeta(cs, params) } - return handleSend[*ListResourcesResult](ctx, methodListResources, newClientRequest(cs, orZero[Params](params))) + result, err := handleSend[*ListResourcesResult](ctx, methodListResources, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.resourcesCache.put(params.Cursor, result) + } + return result, nil } // ListResourceTemplates lists the resource templates that are currently available on the server. func (cs *ClientSession) ListResourceTemplates(ctx context.Context, params *ListResourceTemplatesParams) (*ListResourceTemplatesResult, error) { if cs.usesNewProtocol() { + if result, ok := cachedListResult(&cs.resourceTemplatesCache, params); ok { + return result, nil + } params = injectRequestMeta(cs, params) } - return handleSend[*ListResourceTemplatesResult](ctx, methodListResourceTemplates, newClientRequest(cs, orZero[Params](params))) + result, err := handleSend[*ListResourceTemplatesResult](ctx, methodListResourceTemplates, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.resourceTemplatesCache.put(params.Cursor, result) + } + return result, nil } // ReadResource asks the server to read a resource and return its contents. func (cs *ClientSession) ReadResource(ctx context.Context, params *ReadResourceParams) (*ReadResourceResult, error) { if cs.usesNewProtocol() { + var uri string + if params != nil { + uri = params.URI + } + if result, ok := cs.readResourceCache.get(uri); ok { + return result, nil + } params = injectRequestMeta(cs, params) } - return handleSend[*ReadResourceResult](ctx, methodReadResource, newClientRequest(cs, orZero[Params](params))) + result, err := handleSend[*ReadResourceResult](ctx, methodReadResource, newClientRequest(cs, orZero[Params](params))) + if err != nil { + return nil, err + } + if cs.usesNewProtocol() { + cs.readResourceCache.put(params.URI, result) + } + return result, nil } func (cs *ClientSession) Complete(ctx context.Context, params *CompleteParams) (*CompleteResult, error) { @@ -1235,6 +1293,9 @@ func (cs *ClientSession) Unsubscribe(ctx context.Context, params *UnsubscribePar } func (c *Client) callToolChangedHandler(ctx context.Context, req *ToolListChangedRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok { + cs.toolsCache.invalidate() + } if h := c.opts.ToolListChangedHandler; h != nil { h(ctx, req) } @@ -1242,6 +1303,9 @@ func (c *Client) callToolChangedHandler(ctx context.Context, req *ToolListChange } func (c *Client) callPromptChangedHandler(ctx context.Context, req *PromptListChangedRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok { + cs.promptsCache.invalidate() + } if h := c.opts.PromptListChangedHandler; h != nil { h(ctx, req) } @@ -1249,6 +1313,10 @@ func (c *Client) callPromptChangedHandler(ctx context.Context, req *PromptListCh } func (c *Client) callResourceChangedHandler(ctx context.Context, req *ResourceListChangedRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok { + cs.resourcesCache.invalidate() + cs.resourceTemplatesCache.invalidate() + } if h := c.opts.ResourceListChangedHandler; h != nil { h(ctx, req) } @@ -1256,6 +1324,9 @@ func (c *Client) callResourceChangedHandler(ctx context.Context, req *ResourceLi } func (c *Client) callResourceUpdatedHandler(ctx context.Context, req *ResourceUpdatedNotificationRequest) (Result, error) { + if cs, ok := req.GetSession().(*ClientSession); ok && req.Params != nil { + cs.readResourceCache.invalidateKey(req.Params.URI) + } if h := c.opts.ResourceUpdatedHandler; h != nil { h(ctx, req) } diff --git a/mcp/client_test.go b/mcp/client_test.go index 16b6b760..20dde797 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -444,16 +444,22 @@ func TestClientCapabilities(t *testing.T) { } } -func TestToolCache(t *testing.T) { +func TestLookupTool(t *testing.T) { tool1 := &Tool{Name: "tool1", Description: "first"} tool2 := &Tool{Name: "tool2", Description: "second"} tool1Updated := &Tool{Name: "tool1", Description: "updated"} + // page represents a single cached ListToolsResult, keyed by cursor. + type page struct { + cursor string + tools []*Tool + } + testCases := []struct { - name string - cacheBatches [][]*Tool - lookup string - want *Tool + name string + pages []page + lookup string + want *Tool }{ { name: "empty cache", @@ -461,58 +467,58 @@ func TestToolCache(t *testing.T) { want: nil, }, { - name: "single tool found", - cacheBatches: [][]*Tool{{tool1}}, - lookup: "tool1", - want: tool1, - }, - { - name: "unknown tool", - cacheBatches: [][]*Tool{{tool1}}, - lookup: "nonexistent", - want: nil, + name: "single tool found", + pages: []page{{cursor: "", tools: []*Tool{tool1}}}, + lookup: "tool1", + want: tool1, }, { - name: "multiple tools single batch", - cacheBatches: [][]*Tool{{tool1, tool2}}, - lookup: "tool2", - want: tool2, + name: "unknown tool", + pages: []page{{cursor: "", tools: []*Tool{tool1}}}, + lookup: "nonexistent", + want: nil, }, { - name: "replace clears old entries", - cacheBatches: [][]*Tool{{tool1}, {tool2}}, - lookup: "tool1", - want: nil, + name: "multiple tools single page", + pages: []page{{cursor: "", tools: []*Tool{tool1, tool2}}}, + lookup: "tool2", + want: tool2, }, { - name: "replace keeps new entries", - cacheBatches: [][]*Tool{{tool1}, {tool2}}, - lookup: "tool2", - want: tool2, + name: "tool found across paginated pages", + pages: []page{ + {cursor: "", tools: []*Tool{tool1}}, + {cursor: "page2", tools: []*Tool{tool2}}, + }, + lookup: "tool2", + want: tool2, }, { - name: "overwrite existing entry", - cacheBatches: [][]*Tool{{tool1}, {tool1Updated}}, - lookup: "tool1", - want: tool1Updated, + name: "re-list same cursor overwrites entry", + pages: []page{ + {cursor: "", tools: []*Tool{tool1}}, + {cursor: "", tools: []*Tool{tool1Updated}}, + }, + lookup: "tool1", + want: tool1Updated, }, { - name: "empty batch no-op", - cacheBatches: [][]*Tool{{}}, - lookup: "tool1", - want: nil, + name: "empty page no-op", + pages: []page{{cursor: "", tools: []*Tool{}}}, + lookup: "tool1", + want: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cs := &ClientSession{} - for _, batch := range tc.cacheBatches { - cs.cacheTools(batch) + for _, p := range tc.pages { + cs.toolsCache.put(p.cursor, &ListToolsResult{Tools: p.tools}) } - got := cs.getCachedTool(tc.lookup) + got := cs.lookupTool(tc.lookup) if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("getCachedTool(%q) mismatch (-want +got):\n%s", tc.lookup, diff) + t.Errorf("lookupTool(%q) mismatch (-want +got):\n%s", tc.lookup, diff) } }) } diff --git a/mcp/protocol.go b/mcp/protocol.go index acc7ec48..f374ffbc 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -1100,11 +1100,53 @@ func (x *ListPromptsParams) GetProgressToken() any { return getProgressToken(x) func (x *ListPromptsParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListPromptsParams) cursorPtr() *string { return &x.Cursor } +// CacheableResult is a result that supports a time-to-live (TTL) hint for +// client-side caching. +type CacheableResult interface { + Result + GetTTLMs() int + GetCacheScope() string +} + +// Cacheable describes a result that supports a time-to-live (TTL) hint for +// client-side caching. +type Cacheable struct { + // A hint from the server indicating how long (in milliseconds) the + // client MAY cache this response before re-fetching. Semantics are + // analogous to HTTP Cache-Control max-age. + // + // If 0, the response SHOULD be considered immediately stale. + // If positive, the client SHOULD consider the result fresh for this + // many milliseconds after receiving the response. + TTLMs int `json:"ttlMs"` + + // Indicates the intended scope of the cached response, analogous to + // HTTP Cache-Control: public vs Cache-Control: private. + // + // "public": Any client or intermediary MAY cache and serve the response. + // "private": Only the requesting user's client MAY cache the response. + // + // Defaults to "public" if absent. + CacheScope string `json:"cacheScope"` +} + +// GetTTLMs returns the TTL hint in milliseconds. +func (c Cacheable) GetTTLMs() int { return c.TTLMs } + +// GetCacheScope returns the cache scope. +func (c Cacheable) GetCacheScope() string { return c.CacheScope } + +// setDefaultCacheableValues sets the default values for the cacheable fields. +func (c *Cacheable) setDefaultCacheableValues() { + c.CacheScope = "public" +} + // The server's response to a prompts/list request from the client. type ListPromptsResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -1134,6 +1176,7 @@ type ListResourceTemplatesResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -1163,6 +1206,7 @@ type ListResourcesResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -1217,6 +1261,7 @@ type ListToolsResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` + Cacheable // An opaque token representing the pagination position after the last returned // result. If present, there may be more results available. NextCursor string `json:"nextCursor,omitempty"` @@ -1458,7 +1503,8 @@ func (x *ReadResourceParams) SetProgressToken(t any) { setProgressToken(x, t) } type ReadResourceResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. - Meta `json:"_meta,omitempty"` + Meta `json:"_meta,omitempty"` + Cacheable Contents []*ResourceContents `json:"contents"` // InputRequests is populated when ResultType is ResultTypeInputRequired. diff --git a/mcp/server.go b/mcp/server.go index 912dea98..a115b40a 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -736,12 +736,17 @@ func (s *Server) listPrompts(_ context.Context, req *ListPromptsRequest) (*ListP if req.Params == nil { req.Params = &ListPromptsParams{} } - return paginateList(s.prompts, s.opts.PageSize, req.Params, &ListPromptsResult{}, func(res *ListPromptsResult, prompts []*serverPrompt) { + res, err := paginateList(s.prompts, s.opts.PageSize, req.Params, &ListPromptsResult{}, func(res *ListPromptsResult, prompts []*serverPrompt) { res.Prompts = []*Prompt{} // avoid JSON null for _, p := range prompts { res.Prompts = append(res.Prompts, p.prompt) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } func (s *Server) getPrompt(ctx context.Context, req *GetPromptRequest) (*GetPromptResult, error) { @@ -813,12 +818,17 @@ func (s *Server) listTools(_ context.Context, req *ListToolsRequest) (*ListTools if req.Params == nil { req.Params = &ListToolsParams{} } - return paginateList(s.tools, s.opts.PageSize, req.Params, &ListToolsResult{}, func(res *ListToolsResult, tools []*serverTool) { + res, err := paginateList(s.tools, s.opts.PageSize, req.Params, &ListToolsResult{}, func(res *ListToolsResult, tools []*serverTool) { res.Tools = []*Tool{} // avoid JSON null for _, t := range tools { res.Tools = append(res.Tools, t.tool) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } // getServerTool looks up a server tool by name. @@ -856,12 +866,17 @@ func (s *Server) listResources(_ context.Context, req *ListResourcesRequest) (*L if req.Params == nil { req.Params = &ListResourcesParams{} } - return paginateList(s.resources, s.opts.PageSize, req.Params, &ListResourcesResult{}, func(res *ListResourcesResult, resources []*serverResource) { + res, err := paginateList(s.resources, s.opts.PageSize, req.Params, &ListResourcesResult{}, func(res *ListResourcesResult, resources []*serverResource) { res.Resources = []*Resource{} // avoid JSON null for _, r := range resources { res.Resources = append(res.Resources, r.resource) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } func (s *Server) listResourceTemplates(_ context.Context, req *ListResourceTemplatesRequest) (*ListResourceTemplatesResult, error) { @@ -870,13 +885,18 @@ func (s *Server) listResourceTemplates(_ context.Context, req *ListResourceTempl if req.Params == nil { req.Params = &ListResourceTemplatesParams{} } - return paginateList(s.resourceTemplates, s.opts.PageSize, req.Params, &ListResourceTemplatesResult{}, + res, err := paginateList(s.resourceTemplates, s.opts.PageSize, req.Params, &ListResourceTemplatesResult{}, func(res *ListResourceTemplatesResult, rts []*serverResourceTemplate) { res.ResourceTemplates = []*ResourceTemplate{} // avoid JSON null for _, rt := range rts { res.ResourceTemplates = append(res.ResourceTemplates, rt.resourceTemplate) } }) + if err != nil { + return nil, err + } + res.setDefaultCacheableValues() + return res, nil } func (s *Server) readResource(ctx context.Context, req *ReadResourceRequest) (*ReadResourceResult, error) { @@ -896,6 +916,7 @@ func (s *Server) readResource(ctx context.Context, req *ReadResourceRequest) (*R if err := handleMultiRoundTripResult(req.Session, s.opts.Logger, res); err != nil { return nil, err } + res.setDefaultCacheableValues() if res.resultType == resultTypeInputRequired { return res, nil } diff --git a/mcp/testdata/conformance/server/lifecycle.txtar b/mcp/testdata/conformance/server/lifecycle.txtar index 0a8cf34b..652aa7f3 100644 --- a/mcp/testdata/conformance/server/lifecycle.txtar +++ b/mcp/testdata/conformance/server/lifecycle.txtar @@ -53,6 +53,8 @@ See also modelcontextprotocol/go-sdk#225. "jsonrpc": "2.0", "id": 2, "result": { + "ttlMs": 0, + "cacheScope": "public", "tools": [] } } @@ -60,6 +62,8 @@ See also modelcontextprotocol/go-sdk#225. "jsonrpc": "2.0", "id": 3, "result": { + "ttlMs": 0, + "cacheScope": "public", "tools": [] } } diff --git a/mcp/testdata/conformance/server/prompts.txtar b/mcp/testdata/conformance/server/prompts.txtar index fdaf7932..2d8e1686 100644 --- a/mcp/testdata/conformance/server/prompts.txtar +++ b/mcp/testdata/conformance/server/prompts.txtar @@ -45,6 +45,8 @@ code_review "jsonrpc": "2.0", "id": 2, "result": { + "ttlMs": 0, + "cacheScope": "public", "tools": [] } } @@ -52,6 +54,8 @@ code_review "jsonrpc": "2.0", "id": 4, "result": { + "ttlMs": 0, + "cacheScope": "public", "prompts": [ { "arguments": [ diff --git a/mcp/testdata/conformance/server/resources.txtar b/mcp/testdata/conformance/server/resources.txtar index 314817b8..1c3ed809 100644 --- a/mcp/testdata/conformance/server/resources.txtar +++ b/mcp/testdata/conformance/server/resources.txtar @@ -67,6 +67,8 @@ info.txt "jsonrpc": "2.0", "id": 2, "result": { + "ttlMs": 0, + "cacheScope": "public", "resources": [ { "mimeType": "text/plain", @@ -85,6 +87,8 @@ info.txt "jsonrpc": "2.0", "id": 3, "result": { + "ttlMs": 0, + "cacheScope": "public", "contents": [ { "uri": "embedded:info", @@ -103,6 +107,8 @@ info.txt "jsonrpc": "2.0", "id": 3, "result": { + "ttlMs": 0, + "cacheScope": "public", "contents": [ { "uri": "file:///info.txt", diff --git a/mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar b/mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar index 2add92ef..356be180 100644 --- a/mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar +++ b/mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar @@ -87,6 +87,8 @@ infoWithIcon "jsonrpc": "2.0", "id": 2, "result": { + "ttlMs": 0, + "cacheScope": "public", "tools": [ { "description": "return resourceLink content with Icon", @@ -140,6 +142,8 @@ infoWithIcon "jsonrpc": "2.0", "id": 3, "result": { + "ttlMs": 0, + "cacheScope": "public", "resources": [ { "mimeType": "text/plain", @@ -164,6 +168,8 @@ infoWithIcon "jsonrpc": "2.0", "id": 4, "result": { + "ttlMs": 0, + "cacheScope": "public", "prompts": [ { "arguments": [ diff --git a/mcp/testdata/conformance/server/tools.txtar b/mcp/testdata/conformance/server/tools.txtar index aadad122..ba6fd33e 100644 --- a/mcp/testdata/conformance/server/tools.txtar +++ b/mcp/testdata/conformance/server/tools.txtar @@ -64,6 +64,8 @@ inc "jsonrpc": "2.0", "id": 2, "result": { + "ttlMs": 0, + "cacheScope": "public", "tools": [ { "description": "say hi", @@ -169,6 +171,8 @@ inc "jsonrpc": "2.0", "id": 3, "result": { + "ttlMs": 0, + "cacheScope": "public", "resources": [] } } @@ -176,6 +180,8 @@ inc "jsonrpc": "2.0", "id": 4, "result": { + "ttlMs": 0, + "cacheScope": "public", "prompts": [] } }