mcp: add ttl for list results (SEP-2549) (#1008)

Fixes #994
This commit is contained in:
Guglielmo Colombo 2026-06-18 15:20:21 +02:00 • committed by GitHub
commit 6333aa7c63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 356 additions and 70 deletions

99
mcp/cache.go Normal file
View 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)
}

View file

@ -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)
}

View file

@ -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)
}
})
}

View file

@ -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.

View file

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

View file

@ -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,
})

View file

@ -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": []
}
}

View file

@ -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": [

View file

@ -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",

View file

@ -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": [

View file

@ -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": []
}
}