Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
21 changes: 15 additions & 6 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}
Expand Down
102 changes: 102 additions & 0 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}