parent
71ffcb412a
commit
6333aa7c63
11 changed files with 356 additions and 70 deletions
99
mcp/cache.go
Normal file
99
mcp/cache.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// 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]) forEachValid(f func(R)) {
|
||||
mc.mu.Lock()
|
||||
defer mc.mu.Unlock()
|
||||
for key, entry := range mc.cachedValues {
|
||||
if !entry.isValid() {
|
||||
delete(mc.cachedValues, key)
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
123
mcp/client.go
123
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.forEachValid(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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,61 @@ func TestToolCache(t *testing.T) {
|
|||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single tool found",
|
||||
cacheBatches: [][]*Tool{{tool1}},
|
||||
lookup: "tool1",
|
||||
want: tool1,
|
||||
name: "single tool found",
|
||||
pages: []page{{cursor: "", tools: []*Tool{tool1}}},
|
||||
lookup: "tool1",
|
||||
want: tool1,
|
||||
},
|
||||
{
|
||||
name: "unknown tool",
|
||||
cacheBatches: [][]*Tool{{tool1}},
|
||||
lookup: "nonexistent",
|
||||
want: nil,
|
||||
name: "unknown tool",
|
||||
pages: []page{{cursor: "", tools: []*Tool{tool1}}},
|
||||
lookup: "nonexistent",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple tools single batch",
|
||||
cacheBatches: [][]*Tool{{tool1, tool2}},
|
||||
lookup: "tool2",
|
||||
want: tool2,
|
||||
name: "multiple tools single page",
|
||||
pages: []page{{cursor: "", tools: []*Tool{tool1, tool2}}},
|
||||
lookup: "tool2",
|
||||
want: tool2,
|
||||
},
|
||||
{
|
||||
name: "replace clears old entries",
|
||||
cacheBatches: [][]*Tool{{tool1}, {tool2}},
|
||||
lookup: "tool1",
|
||||
want: nil,
|
||||
name: "tool found across paginated pages",
|
||||
pages: []page{
|
||||
{cursor: "", tools: []*Tool{tool1}},
|
||||
{cursor: "page2", tools: []*Tool{tool2}},
|
||||
},
|
||||
lookup: "tool2",
|
||||
want: tool2,
|
||||
},
|
||||
{
|
||||
name: "replace keeps new entries",
|
||||
cacheBatches: [][]*Tool{{tool1}, {tool2}},
|
||||
lookup: "tool2",
|
||||
want: tool2,
|
||||
name: "re-list same cursor overwrites entry",
|
||||
pages: []page{
|
||||
{cursor: "", tools: []*Tool{tool1}},
|
||||
{cursor: "", tools: []*Tool{tool1Updated}},
|
||||
},
|
||||
lookup: "tool1",
|
||||
want: tool1Updated,
|
||||
},
|
||||
{
|
||||
name: "overwrite existing entry",
|
||||
cacheBatches: [][]*Tool{{tool1}, {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{
|
||||
Cacheable: Cacheable{TTLMs: 60_000},
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1102,11 +1102,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"`
|
||||
|
|
@ -1136,6 +1178,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"`
|
||||
|
|
@ -1165,6 +1208,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"`
|
||||
|
|
@ -1219,6 +1263,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"`
|
||||
|
|
@ -1460,7 +1505,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.
|
||||
|
|
|
|||
|
|
@ -740,12 +740,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) {
|
||||
|
|
@ -817,12 +822,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.
|
||||
|
|
@ -860,12 +870,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) {
|
||||
|
|
@ -874,13 +889,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) {
|
||||
|
|
@ -900,6 +920,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2227,6 +2227,20 @@ func TestStreamableParamHeadersClientSetsHeaders(t *testing.T) {
|
|||
return &CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}, nil
|
||||
})
|
||||
|
||||
// Install a receiving middleware on the server that opts the tools/list
|
||||
// response caching by setting a positive ttlMs.
|
||||
server.AddReceivingMiddleware(func(next MethodHandler) MethodHandler {
|
||||
return func(ctx context.Context, method string, req Request) (Result, error) {
|
||||
res, err := next(ctx, method, req)
|
||||
if err == nil && method == methodListTools {
|
||||
if r, ok := res.(*ListToolsResult); ok {
|
||||
r.TTLMs = 60_000
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
})
|
||||
|
||||
handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, &StreamableHTTPOptions{
|
||||
Stateless: true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
6
mcp/testdata/conformance/server/tools.txtar
vendored
6
mcp/testdata/conformance/server/tools.txtar
vendored
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue