diff --git a/mcp/protocol.go b/mcp/protocol.go index 47cb9cbd..d6f35edc 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -1192,7 +1192,14 @@ func (c Cacheable) GetTTLMs() int { return c.TTLMs } func (c Cacheable) GetCacheScope() string { return c.CacheScope } // setDefaultCacheableValues sets the default values for the cacheable fields. -func (c *Cacheable) setDefaultCacheableValues() { +// If defaults is non-nil, its TTLMs and CacheScope are copied onto c. +// Otherwise CacheScope is set to "public" (TTLMs stays 0 unless already set). +func (c *Cacheable) setDefaultCacheableValues(defaults *Cacheable) { + if defaults != nil { + c.TTLMs = defaults.TTLMs + c.CacheScope = defaults.CacheScope + return + } c.CacheScope = "public" } diff --git a/mcp/server.go b/mcp/server.go index b5805488..8a95e97b 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -169,6 +169,15 @@ type ServerOptions struct { // GetSessionID is not consulted when [StreamableHTTPOptions.Stateless] is // true, since stateless servers do not maintain sessions. GetSessionID func() string + + // DefaultCacheable, if non-nil, supplies the [Cacheable] values stamped on + // SDK-generated results (server/discover, list methods, and resources/read + // after the handler returns). If nil, those results use the historical + // defaults: CacheScope "public" and TTLMs 0. + // + // Receiving middleware can still overwrite Cacheable on a per-result basis + // after the SDK stamps these values. + DefaultCacheable *Cacheable } // NewServer creates a new MCP server. The resulting server has no features: @@ -851,7 +860,7 @@ func (s *Server) listPrompts(_ context.Context, req *ListPromptsRequest) (*ListP if err != nil { return nil, err } - res.setDefaultCacheableValues() + res.setDefaultCacheableValues(s.opts.DefaultCacheable) return res, nil } @@ -905,7 +914,7 @@ func (s *Server) discover(_ context.Context, req *ServerRequest[*DiscoverParams] Capabilities: s.capabilities(), Instructions: s.opts.Instructions, } - res.setDefaultCacheableValues() + res.setDefaultCacheableValues(s.opts.DefaultCacheable) return res, nil } @@ -941,7 +950,7 @@ func (s *Server) listTools(_ context.Context, req *ListToolsRequest) (*ListTools if err != nil { return nil, err } - res.setDefaultCacheableValues() + res.setDefaultCacheableValues(s.opts.DefaultCacheable) return res, nil } @@ -989,7 +998,7 @@ func (s *Server) listResources(_ context.Context, req *ListResourcesRequest) (*L if err != nil { return nil, err } - res.setDefaultCacheableValues() + res.setDefaultCacheableValues(s.opts.DefaultCacheable) return res, nil } @@ -1009,7 +1018,7 @@ func (s *Server) listResourceTemplates(_ context.Context, req *ListResourceTempl if err != nil { return nil, err } - res.setDefaultCacheableValues() + res.setDefaultCacheableValues(s.opts.DefaultCacheable) return res, nil } @@ -1030,7 +1039,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() + res.setDefaultCacheableValues(s.opts.DefaultCacheable) if res.resultType == resultTypeInputRequired { return res, nil } diff --git a/mcp/server_test.go b/mcp/server_test.go index d6ecfed4..a2e7535f 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -1691,3 +1691,105 @@ func TestServerSession_RejectsServerInitiated(t *testing.T) { } } } + +func TestServerDefaultCacheable(t *testing.T) { + ctx := context.Background() + + for _, tc := range []struct { + name string + defaults *Cacheable + want Cacheable + }{ + { + name: "historical defaults", + want: Cacheable{TTLMs: 0, CacheScope: "public"}, + }, + { + name: "private with TTL", + defaults: &Cacheable{TTLMs: 60_000, CacheScope: "private"}, + want: Cacheable{TTLMs: 60_000, CacheScope: "private"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + server := NewServer(testImpl, &ServerOptions{DefaultCacheable: tc.defaults}) + AddTool(server, &Tool{Name: "t", Description: "d"}, + func(context.Context, *CallToolRequest, struct{}) (*CallToolResult, any, error) { + return &CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}, nil, nil + }) + server.AddPrompt(&Prompt{Name: "p"}, func(context.Context, *GetPromptRequest) (*GetPromptResult, error) { + return &GetPromptResult{}, nil + }) + server.AddResource(&Resource{URI: "test://r", Name: "r"}, func(context.Context, *ReadResourceRequest) (*ReadResourceResult, error) { + return &ReadResourceResult{Contents: []*ResourceContents{{URI: "test://r", Text: "x"}}}, nil + }) + server.AddResourceTemplate(&ResourceTemplate{URITemplate: "test://{id}", Name: "rt"}, + func(context.Context, *ReadResourceRequest) (*ReadResourceResult, error) { + return &ReadResourceResult{Contents: []*ResourceContents{{Text: "x"}}}, nil + }) + + ct, st := NewInMemoryTransports() + if _, err := server.Connect(ctx, st, nil); err != nil { + t.Fatal(err) + } + cs, err := NewClient(testImpl, nil).Connect(ctx, ct, nil) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = cs.Close() }) + + check := func(t *testing.T, label string, got Cacheable) { + t.Helper() + if got != tc.want { + t.Errorf("%s Cacheable = %+v, want %+v", label, got, tc.want) + } + } + + tools, err := cs.ListTools(ctx, nil) + if err != nil { + t.Fatal(err) + } + check(t, "ListTools", tools.Cacheable) + + prompts, err := cs.ListPrompts(ctx, nil) + if err != nil { + t.Fatal(err) + } + check(t, "ListPrompts", prompts.Cacheable) + + resources, err := cs.ListResources(ctx, nil) + if err != nil { + t.Fatal(err) + } + check(t, "ListResources", resources.Cacheable) + + templates, err := cs.ListResourceTemplates(ctx, nil) + if err != nil { + t.Fatal(err) + } + check(t, "ListResourceTemplates", templates.Cacheable) + + read, err := cs.ReadResource(ctx, &ReadResourceParams{URI: "test://r"}) + if err != nil { + t.Fatal(err) + } + check(t, "ReadResource", read.Cacheable) + }) + } +} + +func TestSetDefaultCacheableValues(t *testing.T) { + t.Run("nil defaults", func(t *testing.T) { + c := Cacheable{TTLMs: 42} + c.setDefaultCacheableValues(nil) + if c.TTLMs != 42 || c.CacheScope != "public" { + t.Fatalf("got %+v, want TTLMs preserved and CacheScope public", c) + } + }) + t.Run("explicit defaults", func(t *testing.T) { + c := Cacheable{TTLMs: 1, CacheScope: "public"} + c.setDefaultCacheableValues(&Cacheable{TTLMs: 60_000, CacheScope: "private"}) + if c.TTLMs != 60_000 || c.CacheScope != "private" { + t.Fatalf("got %+v, want private/60000", c) + } + }) +}