Repository: ThinkInAIXYZ/go-mcp
Pull request #135: Feat cursor
Author: login: qiangmzsx
Created: 2025-04-28T07:31:23Z | Merged: 2025-05-14T12:19:22Z

## Description
Implement pagination capabilities in the framework according to [pagination](https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination)
Referencing other frameworks' implementations, pagination is disabled by default and can be enabled through WithPagination() when needed.

## Main Changes
The following core methods have been modified:

1. handleRequestWithListPrompts
2. handleRequestWithListResources
3. handleRequestWithListResourceTemplates
4. handleRequestWithListTools

## Testing
Added new test case TestServerHandleForPage to verify pagination functionality.


Diff:
diff --git a/protocol/pagination.go b/protocol/pagination.go
index d04bdd7..0831a38 100644
--- a/protocol/pagination.go
+++ b/protocol/pagination.go
@@ -1,11 +1,57 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	sort.Slice(allElements, func(i, j int) bool {
+		return allElements[i].GetName() < allElements[j].GetName()
+	})
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
+			return nc > cString
+		})
+	}
+	endPos := len(allElements)
+	if len(allElements) > startPos+limit {
+		endPos = startPos + limit
+	}
+	elementsToReturn := allElements[startPos:endPos]
+	// set the next cursor
+	nextCursor := func() Cursor {
+		if len(elementsToReturn) < limit {
+			return ""
+		}
+		element := elementsToReturn[len(elementsToReturn)-1]
+		nc := element.GetName()
+		toString := base64.StdEncoding.EncodeToString([]byte(nc))
+		return Cursor(toString)
+	}()
+	return elementsToReturn, nextCursor, nil
+}
+
 // PaginatedRequest represents a request that supports pagination
 type PaginatedRequest struct {
-	Cursor string `json:"cursor,omitempty"`
+	Cursor Cursor `json:"cursor,omitempty"`
 }
 
 // PaginatedResult represents a response that supports pagination
 type PaginatedResult struct {
-	NextCursor string `json:"nextCursor,omitempty"`
+	NextCursor Cursor `json:"nextCursor,omitempty"`
 }
diff --git a/protocol/pagination_test.go b/protocol/pagination_test.go
new file mode 100644
index 0000000..aa3efc6
--- /dev/null
+++ b/protocol/pagination_test.go
@@ -0,0 +1,113 @@
+package protocol
+
+import (
+	"encoding/base64"
+	"fmt"
+	"reflect"
+	"sort"
+	"testing"
+)
+
+func BenchmarkPaginationLimitForReflect(b *testing.B) {
+	list := getTools(10000)
+	for i := 0; i < b.N; i++ {
+		_, _, _ = PaginationLimitForReflect[*Tool](list, "dG9vbDMz", 10)
+	}
+}
+
+func BenchmarkPaginationLimitForTool(b *testing.B) {
+	list := getTools(10000)
+	for i := 0; i < b.N; i++ {
+		_, _, _ = PaginationLimitForTool(list, "dG9vbDMz", 10)
+	}
+}
+
+func BenchmarkPaginationLimit(b *testing.B) {
+	list := getTools(10000)
+	for i := 0; i < b.N; i++ {
+		_, _, _ = PaginationLimit[*Tool](list, "dG9vbDMz", 10)
+	}
+}
+
+func getTools(length int) []*Tool {
+	list := make([]*Tool, 0, 10000)
+	for i := 0; i < length; i++ {
+		list = append(list, &Tool{
+			Name:        fmt.Sprintf("tool%d", i),
+			Description: fmt.Sprintf("tool%d", i),
+		})
+	}
+	return list
+}
+
+func PaginationLimitForTool(allElements []*Tool, cursor Cursor, limit int) ([]*Tool, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].Name
+			return nc > cString
+		})
+	}
+	endPos := len(allElements)
+	if len(allElements) > startPos+limit {
+		endPos = startPos + limit
+	}
+	elementsToReturn := allElements[startPos:endPos]
+	// set the next cursor
+	nextCursor := func() Cursor {
+		if len(elementsToReturn) < limit {
+			return ""
+		}
+		element := elementsToReturn[len(elementsToReturn)-1]
+		nc := element.Name
+		toString := base64.StdEncoding.EncodeToString([]byte(nc))
+		return Cursor(toString)
+	}()
+	return elementsToReturn, nextCursor, nil
+}
+
+func PaginationLimitForReflect[T any](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			val := reflect.ValueOf(allElements[i])
+			var nc string
+			if val.Kind() == reflect.Ptr {
+				val = val.Elem()
+			}
+			nc = val.FieldByName("Name").String()
+			return nc > cString
+		})
+	}
+	endPos := len(allElements)
+	if len(allElements) > startPos+limit {
+		endPos = startPos + limit
+	}
+	elementsToReturn := allElements[startPos:endPos]
+	// set the next cursor
+	nextCursor := func() Cursor {
+		if len(elementsToReturn) < limit {
+			return ""
+		}
+		element := elementsToReturn[len(elementsToReturn)-1]
+		val := reflect.ValueOf(element)
+		var nc string
+		if val.Kind() == reflect.Ptr {
+			val = val.Elem()
+		}
+		nc = val.FieldByName("Name").String()
+		toString := base64.StdEncoding.EncodeToString([]byte(nc))
+		return Cursor(toString)
+	}()
+	return elementsToReturn, nextCursor, nil
+}
diff --git a/protocol/prompts.go b/protocol/prompts.go
index 53eafe1..01d924c 100644
--- a/protocol/prompts.go
+++ b/protocol/prompts.go
@@ -8,12 +8,14 @@ import (
 )
 
 // ListPromptsRequest represents a request to list available prompts
-type ListPromptsRequest struct{}
+type ListPromptsRequest struct {
+	Cursor Cursor `json:"cursor,omitempty"`
+}
 
 // ListPromptsResult represents the response to a list prompts request
 type ListPromptsResult struct {
 	Prompts    []Prompt `json:"prompts"`
-	NextCursor string   `json:"nextCursor,omitempty"`
+	NextCursor Cursor   `json:"nextCursor,omitempty"`
 }
 
 // Prompt related types
@@ -23,6 +25,10 @@ type Prompt struct {
 	Arguments   []PromptArgument `json:"arguments,omitempty"`
 }
 
+func (p Prompt) GetName() string {
+	return p.Name
+}
+
 type PromptArgument struct {
 	Name        string `json:"name"`
 	Description string `json:"description,omitempty"`
@@ -101,7 +107,7 @@ func NewListPromptsRequest() *ListPromptsRequest {
 }
 
 // NewListPromptsResult creates a new list prompts response
-func NewListPromptsResult(prompts []Prompt, nextCursor string) *ListPromptsResult {
+func NewListPromptsResult(prompts []Prompt, nextCursor Cursor) *ListPromptsResult {
 	return &ListPromptsResult{
 		Prompts:    prompts,
 		NextCursor: nextCursor,
diff --git a/protocol/resources.go b/protocol/resources.go
index e9bdcf9..00f7655 100644
--- a/protocol/resources.go
+++ b/protocol/resources.go
@@ -10,7 +10,9 @@ import (
 )
 
 // ListResourcesRequest Sent from the client to request a list of resources the server has.
-type ListResourcesRequest struct{}
+type ListResourcesRequest struct {
+	Cursor Cursor `json:"cursor,omitempty"`
+}
 
 // ListResourcesResult The server's response to a resources/list request from the client.
 type ListResourcesResult struct {
@@ -19,16 +21,18 @@ type ListResourcesResult struct {
 	 * 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"`
+	NextCursor Cursor `json:"nextCursor,omitempty"`
 }
 
 // ListResourceTemplatesRequest represents a request to list resource templates
-type ListResourceTemplatesRequest struct{}
+type ListResourceTemplatesRequest struct {
+	Cursor Cursor `json:"cursor,omitempty"`
+}
 
 // ListResourceTemplatesResult represents the response to a list resource templates request
 type ListResourceTemplatesResult struct {
 	ResourceTemplates []ResourceTemplate `json:"resourceTemplates"`
-	NextCursor        string             `json:"nextCursor,omitempty"`
+	NextCursor        Cursor             `json:"nextCursor,omitempty"`
 }
 
 // ReadResourceRequest represents a request to read a specific resource
@@ -93,6 +97,10 @@ type Resource struct {
 	Size     int64  `json:"size,omitempty"`
 }
 
+func (r Resource) GetName() string {
+	return r.Name
+}
+
 type ResourceTemplate struct {
 	Annotated
 	Name              string                `json:"name"`
@@ -102,6 +110,10 @@ type ResourceTemplate struct {
 	MimeType          string                `json:"mimeType,omitempty"`
 }
 
+func (t ResourceTemplate) GetName() string {
+	return t.Name
+}
+
 func (t *ResourceTemplate) UnmarshalJSON(data []byte) error {
 	type Alias ResourceTemplate
 	aux := &struct {
@@ -282,7 +294,7 @@ func NewListResourcesRequest() *ListResourcesRequest {
 }
 
 // NewListResourcesResult creates a new list resources response
-func NewListResourcesResult(resources []Resource, nextCursor string) *ListResourcesResult {
+func NewListResourcesResult(resources []Resource, nextCursor Cursor) *ListResourcesResult {
 	return &ListResourcesResult{
 		Resources:  resources,
 		NextCursor: nextCursor,
@@ -295,7 +307,7 @@ func NewListResourceTemplatesRequest() *ListResourceTemplatesRequest {
 }
 
 // NewListResourceTemplatesResult creates a new list resource templates response
-func NewListResourceTemplatesResult(templates []ResourceTemplate, nextCursor string) *ListResourceTemplatesResult {
+func NewListResourceTemplatesResult(templates []ResourceTemplate, nextCursor Cursor) *ListResourceTemplatesResult {
 	return &ListResourceTemplatesResult{
 		ResourceTemplates: templates,
 		NextCursor:        nextCursor,
diff --git a/protocol/tools.go b/protocol/tools.go
index 1ac8601..54787bb 100644
--- a/protocol/tools.go
+++ b/protocol/tools.go
@@ -8,12 +8,14 @@ import (
 )
 
 // ListToolsRequest represents a request to list available tools
-type ListToolsRequest struct{}
+type ListToolsRequest struct {
+	Cursor Cursor `json:"cursor,omitempty"`
+}
 
 // ListToolsResult represents the response to a list tools request
 type ListToolsResult struct {
 	Tools      []*Tool `json:"tools"`
-	NextCursor string  `json:"nextCursor,omitempty"`
+	NextCursor Cursor  `json:"nextCursor,omitempty"`
 }
 
 // Tool represents a tool definition that the client can call
@@ -30,6 +32,14 @@ type Tool struct {
 	RawInputSchema json.RawMessage `json:"-"`
 }
 
+/*func (t *Tool) GetName() string {
+	return t.Name
+}*/
+
+func (t *Tool) GetName() string {
+	return t.Name
+}
+
 func (t *Tool) MarshalJSON() ([]byte, error) {
 	m := make(map[string]interface{}, 3)
 
@@ -204,7 +214,7 @@ func NewListToolsRequest() *ListToolsRequest {
 }
 
 // NewListToolsResult creates a new list tools response
-func NewListToolsResult(tools []*Tool, nextCursor string) *ListToolsResult {
+func NewListToolsResult(tools []*Tool, nextCursor Cursor) *ListToolsResult {
 	return &ListToolsResult{
 		Tools:      tools,
 		NextCursor: nextCursor,
diff --git a/server/handle.go b/server/handle.go
index f02c83f..9f0d35d 100644
--- a/server/handle.go
+++ b/server/handle.go
@@ -66,7 +66,13 @@ func (server *Server) handleRequestWithListPrompts(rawParams json.RawMessage) (*
 		prompts = append(prompts, *entry.prompt)
 		return true
 	})
-
+	if server.paginationLimit > 0 {
+		resourcesToReturn, nextCursor, err := protocol.PaginationLimit[protocol.Prompt](prompts, request.Cursor, server.paginationLimit)
+		return &protocol.ListPromptsResult{
+			Prompts:    resourcesToReturn,
+			NextCursor: nextCursor,
+		}, err
+	}
 	return &protocol.ListPromptsResult{
 		Prompts: prompts,
 	}, nil
@@ -93,7 +99,6 @@ func (server *Server) handleRequestWithListResources(rawParams json.RawMessage)
 	if server.capabilities.Resources == nil {
 		return nil, pkg.ErrServerNotSupport
 	}
-
 	var request *protocol.ListResourcesRequest
 	if len(rawParams) > 0 {
 		if err := pkg.JSONUnmarshal(rawParams, &request); err != nil {
@@ -106,6 +111,13 @@ func (server *Server) handleRequestWithListResources(rawParams json.RawMessage)
 		resources = append(resources, *entry.resource)
 		return true
 	})
+	if server.paginationLimit > 0 {
+		resourcesToReturn, nextCursor, err := protocol.PaginationLimit[protocol.Resource](resources, request.Cursor, server.paginationLimit)
+		return &protocol.ListResourcesResult{
+			Resources:  resourcesToReturn,
+			NextCursor: nextCursor,
+		}, err
+	}
 
 	return &protocol.ListResourcesResult{
 		Resources: resources,
@@ -129,7 +141,13 @@ func (server *Server) handleRequestWithListResourceTemplates(rawParams json.RawM
 		templates = append(templates, *entry.resourceTemplate)
 		return true
 	})
-
+	if server.paginationLimit > 0 {
+		resourcesToReturn, nextCursor, err := protocol.PaginationLimit[protocol.ResourceTemplate](templates, request.Cursor, server.paginationLimit)
+		return &protocol.ListResourceTemplatesResult{
+			ResourceTemplates: resourcesToReturn,
+			NextCursor:        nextCursor,
+		}, err
+	}
 	return &protocol.ListResourceTemplatesResult{
 		ResourceTemplates: templates,
 	}, nil
@@ -222,7 +240,13 @@ func (server *Server) handleRequestWithListTools(rawParams json.RawMessage) (*pr
 		tools = append(tools, entry.tool)
 		return true
 	})
-
+	if server.paginationLimit > 0 {
+		resourcesToReturn, nextCursor, err := protocol.PaginationLimit[*protocol.Tool](tools, request.Cursor, server.paginationLimit)
+		return &protocol.ListToolsResult{
+			Tools:      resourcesToReturn,
+			NextCursor: nextCursor,
+		}, err
+	}
 	return &protocol.ListToolsResult{Tools: tools}, nil
 }
 
diff --git a/server/server.go b/server/server.go
index bbabf8e..ba3f248 100644
--- a/server/server.go
+++ b/server/server.go
@@ -46,6 +46,12 @@ func WithLogger(logger pkg.Logger) Option {
 	}
 }
 
+func WithPagination(limit int) Option {
+	return func(s *Server) {
+		s.paginationLimit = limit
+	}
+}
+
 func WithGenSessionIDFunc(genSessionID func(context.Context) string) Option {
 	return func(s *Server) {
 		s.genSessionID = genSessionID
@@ -69,6 +75,8 @@ type Server struct {
 	serverInfo   *protocol.Implementation
 	instructions string
 
+	paginationLimit int
+
 	logger pkg.Logger
 
 	genSessionID func(ctx context.Context) string
diff --git a/server/server_test.go b/server/server_test.go
index 85be26a..ca9584f 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"reflect"
 	"testing"
@@ -506,3 +507,227 @@ func testServerInit(t *testing.T, server *Server, in io.Writer, outScan *bufio.S
 		t.Fatalf("in Write: %+v", err)
 	}
 }
+
+func TestServerHandleForPage(t *testing.T) {
+	reader1, writer1 := io.Pipe()
+	reader2, writer2 := io.Pipe()
+
+	var (
+		in = struct {
+			reader io.ReadCloser
+			writer io.WriteCloser
+		}{
+			reader: reader1,
+			writer: writer1,
+		}
+
+		out = struct {
+			reader io.ReadCloser
+			writer io.WriteCloser
+		}{
+			reader: reader2,
+			writer: writer2,
+		}
+
+		outScan = bufio.NewScanner(out.reader)
+	)
+
+	server, err := NewServer(
+		transport.NewMockServerTransport(in.reader, out.writer),
+		WithServerInfo(protocol.Implementation{
+			Name:    "ExampleServer",
+			Version: "1.0.0",
+		}),
+		WithPagination(5),
+	)
+	if err != nil {
+		t.Fatalf("NewServer: %+v", err)
+	}
+	total := 10
+	// add tool
+	for i := 0; i < total; i++ {
+		testTool, err := protocol.NewTool(fmt.Sprintf("test_tool_%d", i), fmt.Sprintf("test_tool_%d", i), currentTimeReq{})
+		if err != nil {
+			t.Fatalf("NewTool: %+v", err)
+			return
+		}
+
+		testToolCallContent := &protocol.TextContent{
+			Type: "text",
+			Text: fmt.Sprintf("pong_%d", i),
+		}
+		server.RegisterTool(testTool, func(_ context.Context, _ *protocol.CallToolRequest) (*protocol.CallToolResult, error) {
+			return &protocol.CallToolResult{
+				Content: []protocol.Content{testToolCallContent},
+			}, nil
+		})
+	}
+
+	// add prompt
+	for i := 0; i < total; i++ {
+		testPrompt := &protocol.Prompt{
+			Name:        fmt.Sprintf("test_prompt_%d", i),
+			Description: fmt.Sprintf("test_prompt_description_%d", i),
+			Arguments: []protocol.PromptArgument{
+				{
+					Name:        "params1",
+					Description: "params1's description",
+					Required:    true,
+				},
+			},
+		}
+		testPromptGetResponse := &protocol.GetPromptResult{
+			Description: fmt.Sprintf("test_prompt_description_%d", i),
+		}
+		server.RegisterPrompt(testPrompt, func(context.Context, *protocol.GetPromptRequest) (*protocol.GetPromptResult, error) {
+			return testPromptGetResponse, nil
+		})
+	}
+	// add resource
+	for i := 0; i < total; i++ {
+		testResource := &protocol.Resource{
+			URI:      fmt.Sprintf("file:///test%d.txt", i),
+			Name:     fmt.Sprintf("test%d.txt", i),
+			MimeType: "text/plain-txt",
+		}
+		testResourceContent := protocol.TextResourceContents{
+			URI:      testResource.URI,
+			MimeType: testResource.MimeType,
+			Text:     fmt.Sprintf("test%d", i),
+		}
+		server.RegisterResource(testResource, func(context.Context, *protocol.ReadResourceRequest) (*protocol.ReadResourceResult, error) {
+			return &protocol.ReadResourceResult{
+				Contents: []protocol.ResourceContents{
+					testResourceContent,
+				},
+			}, nil
+		})
+	}
+	// add resource template
+	for i := 0; i < total; i++ {
+		testResourceTemplate := &protocol.ResourceTemplate{
+			URITemplate: fmt.Sprintf("file:///{path}/%d", i),
+			Name:        fmt.Sprintf("test_%d", i),
+		}
+		testResourceContent := protocol.TextResourceContents{
+			URI:      fmt.Sprintf("file:///test%d.txt", i),
+			MimeType: "text/plain-txt",
+			Text:     fmt.Sprintf("test%d", i),
+		}
+		if err := server.RegisterResourceTemplate(testResourceTemplate, func(context.Context, *protocol.ReadResourceRequest) (*protocol.ReadResourceResult, error) {
+			return &protocol.ReadResourceResult{
+				Contents: []protocol.ResourceContents{
+					testResourceContent,
+				},
+			}, nil
+		}); err != nil {
+			t.Fatalf("RegisterResourceTemplate: %+v", err)
+			return
+		}
+	}
+	go func() {
+		if err := server.Run(); err != nil {
+			t.Errorf("server start: %+v", err)
+		}
+	}()
+
+	testServerInit(t, server, in.writer, outScan)
+
+	tests := []struct {
+		name             string
+		method           protocol.Method
+		request          protocol.ClientRequest
+		expectedResponse protocol.ServerResponse
+	}{
+		{
+			name:             "test_list_tool",
+			method:           protocol.ToolsList,
+			request:          protocol.ListToolsRequest{},
+			expectedResponse: protocol.ListToolsResult{NextCursor: ""},
+		},
+		{
+			name:    "test_list_prompt",
+			method:  protocol.PromptsList,
+			request: protocol.ListPromptsRequest{},
+			expectedResponse: protocol.ListPromptsResult{
+				NextCursor: "",
+			},
+		},
+		{
+			name:    "test_list_resource",
+			method:  protocol.ResourcesList,
+			request: protocol.ListResourcesRequest{},
+			expectedResponse: protocol.ListResourcesResult{
+				NextCursor: "",
+			},
+		},
+		{
+			name:    "test_list_resource_template",
+			method:  protocol.ResourceListTemplates,
+			request: protocol.ListResourceTemplatesRequest{},
+			expectedResponse: protocol.ListResourceTemplatesResult{
+				NextCursor: "",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			uuid, _ := uuid.NewUUID()
+			totalResq := 0
+			req := protocol.NewJSONRPCRequest(uuid, tt.method, tt.request)
+			for i := 0; i < 3; i++ {
+				reqBytes, err := json.Marshal(req)
+				if err != nil {
+					t.Fatalf("json Marshal: %+v", err)
+				}
+				if _, err = in.writer.Write(append(reqBytes, "\n"...)); err != nil {
+					t.Fatalf("in Write: %+v", err)
+				}
+
+				var respBytes []byte
+				if outScan.Scan() {
+					respBytes = outScan.Bytes()
+					if outScan.Err() != nil {
+						t.Fatalf("outScan: %+v", err)
+					}
+				}
+				respStruct := struct {
+					Jsonrpc string      `json:"jsonrpc"`
+					ID      interface{} `json:"id"`
+					Result  struct {
+						NextCursor        string        `json:"nextCursor"`
+						Tools             []interface{} `json:"tools"`
+						Prompts           []interface{} `json:"prompts"`
+						Resources         []interface{} `json:"resources"`
+						ResourceTemplates []interface{} `json:"resourceTemplates"`
+					} `json:"result"`
+				}{}
+				if err = pkg.JSONUnmarshal(respBytes, &respStruct); err != nil {
+					t.Fatal(err)
+				}
+
+				if respStruct.Result.NextCursor == "" {
+					break
+				}
+				switch tt.method {
+				case protocol.ToolsList:
+					tt.request = protocol.ListToolsRequest{Cursor: protocol.Cursor(respStruct.Result.NextCursor)}
+				case protocol.PromptsList:
+					tt.request = protocol.ListPromptsRequest{Cursor: protocol.Cursor(respStruct.Result.NextCursor)}
+				case protocol.ResourceListTemplates:
+					tt.request = protocol.ListResourceTemplatesRequest{Cursor: protocol.Cursor(respStruct.Result.NextCursor)}
+				case protocol.ResourcesList:
+					tt.request = protocol.ListResourcesRequest{Cursor: protocol.Cursor(respStruct.Result.NextCursor)}
+				}
+				req = protocol.NewJSONRPCRequest(uuid, tt.method, tt.request)
+				totalResq = totalResq + len(respStruct.Result.Tools) + len(respStruct.Result.Prompts) +
+					len(respStruct.Result.Resources) + len(respStruct.Result.ResourceTemplates)
+			}
+			if total != totalResq {
+				t.Fatalf("totalResq: %d, total: %d", totalResq, total)
+			}
+			// t.Logf("totalResq: %d,total:%d", totalResq, total)
+		})
+	}
+}


Reviews:
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T05:22:54Z
user: login: qiangmzsx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:09:40Z
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T15:30:38Z
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:13:36Z
user: login: qiangmzsx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T04:02:15Z
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T09:07:17Z
user: login: qiangmzsx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:46:40Z
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T13:03:16Z
user: login: qiangmzsx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:42:48Z
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T02:27:08Z
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T02:27:37Z
user: login: qiangmzsx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T05:17:35Z
user: login: jesse-engineer
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T10:14:53Z
user: login: qiangmzsx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T02:34:35Z

Review comments:
diff_hunk: @@ -1,11 +1,61 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"reflect"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+func PaginationLimit[T any](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			val := reflect.ValueOf(allElements[i])
+			var nc string
+			if val.Kind() == reflect.Ptr {
+				val = val.Elem()
+			}
+			nc = val.FieldByName("Name").String()
+			return nc > cString
+		})
+	}
+	endPos := len(allElements)
+	if len(allElements) > startPos+limit {
+		endPos = startPos + limit
+	}
+	elementsToReturn := allElements[startPos:endPos]
+	// set the next cursor
+	nextCursor := func() Cursor {
+		if len(elementsToReturn) >= limit {
path: protocol/pagination.go
user: login: jesse-engineer
body: This can be optimized to return early.
if len(elementsToReturn) < limit {
    return ""
}
xxx
created_at: 2025-04-30T05:17:47Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,61 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"reflect"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+func PaginationLimit[T any](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			val := reflect.ValueOf(allElements[i])
path: protocol/pagination.go
user: login: jesse-engineer
body: A large number of reflection operations are used here, which may cause performance problems when the amount of data or traffic is large, and other methods are needed to obtain values.
created_at: 2025-04-30T05:22:48Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,61 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"reflect"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+func PaginationLimit[T any](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			val := reflect.ValueOf(allElements[i])
path: protocol/pagination.go
user: login: qiangmzsx
body: I don't see significant performance concerns with reflection in this scenario. Let's analyze the benchmark results:
```go
package protocol

import (
	"encoding/base64"
	"fmt"
	"sort"
	"testing"
)

func BenchmarkPaginationLimit(b *testing.B) {
	list := getTools(10000)
	for i := 0; i < b.N; i++ {
		PaginationLimit[Tool](list, "dG9vbDMz", 10)
	}
}

func BenchmarkPaginationLimitForTool(b *testing.B) {
	list := getTools(10000)
	for i := 0; i < b.N; i++ {
		PaginationLimitForTool(list, "dG9vbDMz", 10)
	}
}

func getTools(length int) []Tool {
	list := make([]Tool, 0, 10000)
	for i := 0; i < length; i++ {
		list = append(list, Tool{
			Name:        fmt.Sprintf("tool%d", i),
			Description: fmt.Sprintf("tool%d", i),
		})
	}
	return list
}

func PaginationLimitForTool(allElements []Tool, cursor Cursor, limit int) ([]Tool, Cursor, error) {
	startPos := 0
	if cursor != "" {
		c, err := base64.StdEncoding.DecodeString(string(cursor))
		if err != nil {
			return nil, "", err
		}
		cString := string(c)
		startPos = sort.Search(len(allElements), func(i int) bool {
			nc := allElements[i].Name
			return nc > cString
		})
	}
	endPos := len(allElements)
	if len(allElements) > startPos+limit {
		endPos = startPos + limit
	}
	elementsToReturn := allElements[startPos:endPos]
	// set the next cursor
	nextCursor := func() Cursor {
		if len(elementsToReturn) < limit {
			return ""
		}
		element := elementsToReturn[len(elementsToReturn)-1]
		nc := element.Name
		toString := base64.StdEncoding.EncodeToString([]byte(nc))
		return Cursor(toString)
	}()
	return elementsToReturn, nextCursor, nil
}

```
» go test -bench . ouerqiang@qiangmzsx
goos: darwin
goarch: arm64
pkg: github.com/ThinkInAIXYZ/go-mcp/protocol
cpu: Apple M3 Pro
BenchmarkPaginationLimit-11 2369064 506.2 ns/op
BenchmarkPaginationLimitForTool-11 12925254 91.49 ns/op
PASS
ok github.com/ThinkInAIXYZ/go-mcp/protocol 3.159s

While there's a 5.5x performance difference between the reflection-based (506.2 ns/op) and direct implementation (91.49 ns/op), this gap is relatively manageable for pagination operations that typically occur during API interactions. The reflection overhead becomes negligible when considering:

- Actual API response times are dominated by network I/O and database operations
- Pagination is usually executed infrequently compared to core business logic
- Modern CPUs handle type assertions and interface conversions efficiently

The reflection approach provides critical maintainability benefits:

- Eliminates code duplication across different struct types
- Simplifies future schema changes through single implementation point
- Maintains consistent API behavior for all paginated collections
For comparison, typical pagination implementations in the Go ecosystem (like Kubernetes list operations) show similar performance profiles where reflection-based solutions are accepted within 2-5x baseline costs. Given our use case requirements, this performance profile is acceptable while providing superior code maintainability.
created_at: 2025-04-30T07:09:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,61 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"reflect"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+func PaginationLimit[T any](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			val := reflect.ValueOf(allElements[i])
path: protocol/pagination.go
user: login: jesse-engineer
body: There are three problems with this implementation:
1. The allElements passed in need to be sorted, and this information is implicit;
2. The passed in type T needs to contain the Name field, and the Name field is the unique identifier of type T, and this information is also implicit;
3. Based on the reflection implementation, the performance problem focuses not on time consumption, but on CPU usage. This problem is common in the json standard library and protobuf official library. This is also the original intention of most third-party json and protobuf libraries (sonic, json-iterator, gogoprotobuf, etc.), that is, to optimize the CPU overhead caused by the extensive use of reflection when serializing and deserializing the json standard library and pb official library.

So here, if we let Tool, Resource, Prompt and other types implement a method that can obtain a unique ID, and then pass Tool, Resource, and Prompt in the form of an interface, the above three problems can be solved.
created_at: 2025-04-30T15:30:31Z
author_association: COLLABORATOR
diff_hunk: @@ -106,6 +115,16 @@ func (server *Server) handleRequestWithListResources(rawParams json.RawMessage)
 		resources = append(resources, *entry.resource)
 		return true
 	})
+	if server.paginationLimit > 0 {
+		sort.Slice(resources, func(i, j int) bool {
path: server/handle.go
user: login: jesse-engineer
body: Tool, resource, and prompt have all implemented the named interface, so you can put this sorting operation into the PaginationLimit method.
created_at: 2025-05-07T02:11:16Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,54 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
path: protocol/pagination.go
user: login: jesse-engineer
body: Is there a bug here? If a new name is registered before cString, it will not be returned here.
created_at: 2025-05-07T02:13:33Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,54 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
path: protocol/pagination.go
user: login: qiangmzsx
body: > Is there a bug here? If a new name is registered before cString, it will not be returned here.

If the tool's name is deleted or modified subsequently, the corresponding name lookup will fail. In this case, pagination will automatically reset to start from index 0.

Similarly, if an invalid or non-existent cursor is provided, the system will also initiate pagination from index 0.

This behavior is by design and aligns with our expected system logic. The reset mechanism ensures consistent data retrieval when identifiers become invalid or stale.
created_at: 2025-05-07T04:02:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,54 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
path: protocol/pagination.go
user: login: jesse-engineer
body: The point I want to express is not this issue, but rather if a new tool is inserted before the last tool name during the intervals of the client querying the tool list multiple times, the client will not be able to perceive this and the information about this tool will be lost.
created_at: 2025-05-07T09:07:16Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,54 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
path: protocol/pagination.go
user: login: qiangmzsx
body: > The point I want to express is not this issue, but rather if a new tool is inserted before the last tool name during the intervals of the client querying the tool list multiple times, the client will not be able to perceive this and the information about this tool will be lost.

All pagination implementations inherently exhibit this issue. 
For instance, GitHub repository pagination encounters the same challenge. 
The standardized solution across all scenarios is to reset pagination to its initial state (i.e., page 0).
created_at: 2025-05-07T11:46:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,54 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
path: protocol/pagination.go
user: login: jesse-engineer
body: If sorting is done using the entity's auto-incrementing ID, this problem can be avoided, but our entities (tool, resource, prompt) do not have this. Perhaps we can introduce an entity array appended in the order of registration to achieve the same effect.
created_at: 2025-05-07T13:03:16Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,54 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
path: protocol/pagination.go
user: login: qiangmzsx
body: > If sorting is done using the entity's auto-incrementing ID, this problem can be avoided, but our entities (tool, resource, prompt) do not have this. Perhaps we can introduce an entity array appended in the order of registration to achieve the same effect.

I don't think this needs to be implemented with such complexity. Reasons:

When pagination is enabled, the client should rapidly traverse and retrieve all items through sequential requests
Dynamic entity creation in server-side is relatively rare operation
If still required, we could add an id field as sorting criteria in entities. The current implementation already supports this, though I don't recommend it as:

It introduces unnecessary state management overhead
Violates RESTful principles of stateless pagination
Creates potential consistency issues during concurrent modifications
Alternative approach maintains alignment with standard pagination patterns while preserving system simplicity.
created_at: 2025-05-07T13:42:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,54 @@
 package protocol
 
+import (
+	"encoding/base64"
+	"sort"
+)
+
+// Cursor is an opaque token used to represent a cursor for pagination.
+type Cursor string
+
+type Named interface {
+	GetName() string
+}
+
+func PaginationLimit[T Named](allElements []T, cursor Cursor, limit int) ([]T, Cursor, error) {
+	startPos := 0
+	if cursor != "" {
+		c, err := base64.StdEncoding.DecodeString(string(cursor))
+		if err != nil {
+			return nil, "", err
+		}
+		cString := string(c)
+		startPos = sort.Search(len(allElements), func(i int) bool {
+			nc := allElements[i].GetName()
path: protocol/pagination.go
user: login: jesse-engineer
body: I agree with your point of view.
created_at: 2025-05-08T02:27:08Z
author_association: COLLABORATOR
diff_hunk: @@ -106,6 +115,16 @@ func (server *Server) handleRequestWithListResources(rawParams json.RawMessage)
 		resources = append(resources, *entry.resource)
 		return true
 	})
+	if server.paginationLimit > 0 {
+		sort.Slice(resources, func(i, j int) bool {
path: server/handle.go
user: login: jesse-engineer
body: @qiangmzsx Can this be optimized?
created_at: 2025-05-08T02:27:36Z
author_association: COLLABORATOR
diff_hunk: @@ -106,6 +115,16 @@ func (server *Server) handleRequestWithListResources(rawParams json.RawMessage)
 		resources = append(resources, *entry.resource)
 		return true
 	})
+	if server.paginationLimit > 0 {
+		sort.Slice(resources, func(i, j int) bool {
path: server/handle.go
user: login: qiangmzsx
body: > @qiangmzsx Can this be optimized?

```
func WithPagination(limit int) Option {
	return func(s *Server) {
		s.paginationLimit = limit
	}
}
```
The WithPagination function enables the pagination feature. By default, paginationLimit is set to 0, which means pagination is disabled. When paginationLimit is set to a value greater than 0, pagination is enabled and the number of elements per page is specified.
created_at: 2025-05-11T05:17:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,6 +115,16 @@ func (server *Server) handleRequestWithListResources(rawParams json.RawMessage)
 		resources = append(resources, *entry.resource)
 		return true
 	})
+	if server.paginationLimit > 0 {
+		sort.Slice(resources, func(i, j int) bool {
path: server/handle.go
user: login: jesse-engineer
body: What I want to express is that the sort operations of tool, resource and prompt can be merged into the PaginationLimit method.
created_at: 2025-05-12T10:14:53Z
author_association: COLLABORATOR
diff_hunk: @@ -106,6 +115,16 @@ func (server *Server) handleRequestWithListResources(rawParams json.RawMessage)
 		resources = append(resources, *entry.resource)
 		return true
 	})
+	if server.paginationLimit > 0 {
+		sort.Slice(resources, func(i, j int) bool {
path: server/handle.go
user: login: qiangmzsx
body: > What I want to express is that the sort operations of tool, resource and prompt can be merged into the PaginationLimit method.

The current implementation now allows the sort operation to be incorporated into the PaginationLimit function.
created_at: 2025-05-13T02:34:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov-commenter
created_at: 2025-04-28T07:34:31Z
body: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Attention: Patch coverage is `43.24324%` with `42 lines` in your changes missing coverage. Please review.
| [Files with missing lines](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Patch % | Lines |
|---|---|---|
| [protocol/pagination.go](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHJvdG9jb2wvcGFnaW5hdGlvbi5nbw==) | 0.00% | [30 Missing :warning: ](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [protocol/resources.go](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHJvdG9jb2wvcmVzb3VyY2VzLmdv) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [protocol/prompts.go](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHJvdG9jb2wvcHJvbXB0cy5nbw==) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [protocol/tools.go](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHJvdG9jb2wvdG9vbHMuZ28=) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/ThinkInAIXYZ/go-mcp/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: jesse-engineer
created_at: 2025-04-28T10:57:23Z
body: @qiangmzsx Lint check fail, need fix
author_association: COLLABORATOR
user: login: qiangmzsx
created_at: 2025-05-01T07:09:09Z
body: I previously implemented another method with better performance.It just requires adding a`GetName`method to structs like`Tool`and`Resource`to make them of the`Named`type.

There are pros and cons to this approach.

The advantage is that it reduces the use of reflection.Moreover,if there are new structs in the future,they can also implement this method to become`Named`types,which makes it more versatile.

The downside is that it requires modifying the`Tool`,`Resource`,and other structs.
author_association: CONTRIBUTOR
user: login: jesse-engineer
created_at: 2025-05-07T02:16:15Z
body: > The downside is that it requires modifying the`Tool`,`Resource`,and other structs.

I think this downside is acceptable. it does not change the fields of other structs, but just adds a very simple additional method.


author_association: COLLABORATOR
user: login: jesse-engineer
created_at: 2025-05-14T10:34:59Z
body: @qiangmzsx I don't think there is any problem with this pr, but there is another conflict that needs to be solved. After solving it, I can operate the merge.
author_association: COLLABORATOR
user: login: qiangmzsx
created_at: 2025-05-14T11:55:35Z
body: > @qiangmzsx I don't think there is any problem with this pr, but there is another conflict that needs to be solved. After solving it, I can operate the merge.

@jesse-engineer done!
author_association: CONTRIBUTOR