From 77970c46f2c113cd6ea389acb7090c77be83857a Mon Sep 17 00:00:00 2001 From: "Slach (aider)" Date: Wed, 25 Jun 2025 20:47:31 +0400 Subject: [PATCH 1/8] feat: implement AddResourceTemplate and add resource template test --- mcptest/mcptest.go | 30 +++++++++++++++++-- mcptest/mcptest_test.go | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/mcptest/mcptest.go b/mcptest/mcptest.go index 232eac5df..affdc2237 100644 --- a/mcptest/mcptest.go +++ b/mcptest/mcptest.go @@ -20,9 +20,10 @@ import ( type Server struct { name string - tools []server.ServerTool - prompts []server.ServerPrompt - resources []server.ServerResource + tools []server.ServerTool + prompts []server.ServerPrompt + resources []server.ServerResource + resourceTemplates []server.ServerResourceTemplate cancel func() @@ -106,6 +107,25 @@ func (s *Server) AddResources(resources ...server.ServerResource) { s.resources = append(s.resources, resources...) } +// ServerResourceTemplate combines a ResourceTemplate with its handler function. +type ServerResourceTemplate struct { + Template mcp.ResourceTemplate + Handler server.ResourceTemplateHandlerFunc +} + +// AddResourceTemplate adds a resource template to an unstarted server. +func (s *Server) AddResourceTemplate(template mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) { + s.resourceTemplates = append(s.resourceTemplates, ServerResourceTemplate{ + Template: template, + Handler: handler, + }) +} + +// AddResourceTemplates adds multiple resource templates to an unstarted server. +func (s *Server) AddResourceTemplates(templates ...ServerResourceTemplate) { + s.resourceTemplates = append(s.resourceTemplates, templates...) +} + // Start starts the server in a goroutine. Make sure to defer Close() after Start(). // When using NewServer(), the returned server is already started. func (s *Server) Start(ctx context.Context) error { @@ -122,6 +142,10 @@ func (s *Server) Start(ctx context.Context) error { mcpServer.AddTools(s.tools...) mcpServer.AddPrompts(s.prompts...) mcpServer.AddResources(s.resources...) + + for _, template := range s.resourceTemplates { + mcpServer.AddResourceTemplate(template.Template, template.Handler) + } logger := log.New(&s.logBuffer, "", 0) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index 0ab9b276e..8b4f97019 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -187,3 +187,69 @@ func TestServerWithResource(t *testing.T) { t.Errorf("Got %q, want %q", textContent.Text, want) } } + +func TestServerWithResourceTemplate(t *testing.T) { + ctx := context.Background() + + srv := mcptest.NewUnstartedServer(t) + defer srv.Close() + + // Create a URI template for files like "file://users/{userId}/documents/{docId}" + uriTemplate, err := mcp.NewURITemplate("file://users/{userId}/documents/{docId}") + if err != nil { + t.Fatal("NewURITemplate:", err) + } + + template := mcp.ResourceTemplate{ + URITemplate: uriTemplate, + Name: "User Document", + Description: "A user's document", + MIMEType: "text/plain", + } + + handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + // Extract template variables from the request arguments + userId, ok := request.Params.Arguments["userId"].(string) + if !ok { + return nil, fmt.Errorf("missing userId argument") + } + docId, ok := request.Params.Arguments["docId"].(string) + if !ok { + return nil, fmt.Errorf("missing docId argument") + } + + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: request.Params.URI, + MIMEType: "text/plain", + Text: fmt.Sprintf("Document %s for user %s", docId, userId), + }, + }, nil + } + + srv.AddResourceTemplate(template, handler) + + err = srv.Start(ctx) + if err != nil { + t.Fatal(err) + } + + // Test reading a resource that matches the template + var readReq mcp.ReadResourceRequest + readReq.Params.URI = "file://users/john/documents/readme.txt" + readResult, err := srv.Client().ReadResource(ctx, readReq) + if err != nil { + t.Fatal("ReadResource:", err) + } + if len(readResult.Contents) != 1 { + t.Fatalf("Expected 1 content, got %d", len(readResult.Contents)) + } + textContent, ok := readResult.Contents[0].(mcp.TextResourceContents) + if !ok { + t.Fatalf("Expected TextResourceContents, got %T", readResult.Contents[0]) + } + want := "Document readme.txt for user john" + if textContent.Text != want { + t.Errorf("Got %q, want %q", textContent.Text, want) + } +} From 4e3a1d87caec83c3b9fdca6ea0a9fe1880c12c5d Mon Sep 17 00:00:00 2001 From: "Slach (aider)" Date: Wed, 25 Jun 2025 21:13:12 +0400 Subject: [PATCH 2/8] fix: use local ServerResourceTemplate type in mcptest --- mcptest/mcptest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcptest/mcptest.go b/mcptest/mcptest.go index affdc2237..bc7ccc0fa 100644 --- a/mcptest/mcptest.go +++ b/mcptest/mcptest.go @@ -23,7 +23,7 @@ type Server struct { tools []server.ServerTool prompts []server.ServerPrompt resources []server.ServerResource - resourceTemplates []server.ServerResourceTemplate + resourceTemplates []ServerResourceTemplate cancel func() From 2af60c4c77faf15fd8855f36c8e8b414d1687e9d Mon Sep 17 00:00:00 2001 From: "Slach (aider)" Date: Wed, 25 Jun 2025 21:16:47 +0400 Subject: [PATCH 3/8] fix: replace NewURITemplate with UnmarshalText for URI template creation --- mcptest/mcptest_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index 8b4f97019..b6131d01d 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -195,9 +195,9 @@ func TestServerWithResourceTemplate(t *testing.T) { defer srv.Close() // Create a URI template for files like "file://users/{userId}/documents/{docId}" - uriTemplate, err := mcp.NewURITemplate("file://users/{userId}/documents/{docId}") - if err != nil { - t.Fatal("NewURITemplate:", err) + uriTemplate := &mcp.URITemplate{} + if err := uriTemplate.UnmarshalText([]byte("file://users/{userId}/documents/{docId}")); err != nil { + t.Fatal("URITemplate.UnmarshalText:", err) } template := mcp.ResourceTemplate{ From c371d83b155e9e5fe1f0355462ed4169ac2464ae Mon Sep 17 00:00:00 2001 From: "Slach (aider)" Date: Wed, 25 Jun 2025 21:29:31 +0400 Subject: [PATCH 4/8] fix: correct URITemplate unmarshaling and err variable declaration in tests --- mcptest/mcptest_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index b6131d01d..ecac6fea6 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -196,8 +196,8 @@ func TestServerWithResourceTemplate(t *testing.T) { // Create a URI template for files like "file://users/{userId}/documents/{docId}" uriTemplate := &mcp.URITemplate{} - if err := uriTemplate.UnmarshalText([]byte("file://users/{userId}/documents/{docId}")); err != nil { - t.Fatal("URITemplate.UnmarshalText:", err) + if err := uriTemplate.UnmarshalJSON([]byte(`"file://users/{userId}/documents/{docId}"`)); err != nil { + t.Fatal("URITemplate.UnmarshalJSON:", err) } template := mcp.ResourceTemplate{ @@ -229,7 +229,7 @@ func TestServerWithResourceTemplate(t *testing.T) { srv.AddResourceTemplate(template, handler) - err = srv.Start(ctx) + err := srv.Start(ctx) if err != nil { t.Fatal(err) } From c9bdf2ab91d81498bffea9009713dc3c807e353e Mon Sep 17 00:00:00 2001 From: "Slach (aider)" Date: Wed, 25 Jun 2025 21:32:35 +0400 Subject: [PATCH 5/8] fix: improve resource template argument extraction in test handler --- mcptest/mcptest_test.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index ecac6fea6..a2f86cdaf 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -208,14 +208,34 @@ func TestServerWithResourceTemplate(t *testing.T) { } handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - // Extract template variables from the request arguments - userId, ok := request.Params.Arguments["userId"].(string) - if !ok { - return nil, fmt.Errorf("missing userId argument") + // For this test, we'll extract the values directly from the URI since template matching + // should have populated the arguments, but we'll also handle the case where we need to parse the URI + var userId, docId string + + if request.Params.Arguments != nil { + if uid, ok := request.Params.Arguments["userId"].(string); ok { + userId = uid + } + if did, ok := request.Params.Arguments["docId"].(string); ok { + docId = did + } } - docId, ok := request.Params.Arguments["docId"].(string) - if !ok { - return nil, fmt.Errorf("missing docId argument") + + // If arguments weren't extracted, parse from URI as fallback + if userId == "" || docId == "" { + // Parse "file://users/john/documents/readme.txt" to extract john and readme.txt + uri := request.Params.URI + if len(uri) > 13 && uri[:13] == "file://users/" { + parts := uri[13:] // Remove "file://users/" + if idx := strings.Index(parts, "/documents/"); idx > 0 { + userId = parts[:idx] + docId = parts[idx+11:] // Remove "/documents/" + } + } + } + + if userId == "" || docId == "" { + return nil, fmt.Errorf("could not extract userId and docId from URI: %s", request.Params.URI) } return []mcp.ResourceContents{ From 9ad9552066f4903bb35c8a837bfd584bd018086b Mon Sep 17 00:00:00 2001 From: "Slach (aider)" Date: Thu, 26 Jun 2025 07:26:10 +0400 Subject: [PATCH 6/8] test: add assertions to verify URI template argument population --- mcptest/mcptest_test.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index a2f86cdaf..b0971d72d 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -208,17 +208,25 @@ func TestServerWithResourceTemplate(t *testing.T) { } handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - // For this test, we'll extract the values directly from the URI since template matching - // should have populated the arguments, but we'll also handle the case where we need to parse the URI - var userId, docId string - - if request.Params.Arguments != nil { - if uid, ok := request.Params.Arguments["userId"].(string); ok { - userId = uid - } - if did, ok := request.Params.Arguments["docId"].(string); ok { - docId = did - } + // First verify the arguments were correctly extracted from the URI template + if request.Params.Arguments == nil { + return nil, fmt.Errorf("expected arguments to be populated from URI template") + } + + userId, ok := request.Params.Arguments["userId"].(string) + if !ok { + return nil, fmt.Errorf("expected userId argument to be populated from URI template") + } + if userId != "john" { + return nil, fmt.Errorf("expected userId argument to be 'john', got %q", userId) + } + + docId, ok := request.Params.Arguments["docId"].(string) + if !ok { + return nil, fmt.Errorf("expected docId argument to be populated from URI template") + } + if docId != "readme.txt" { + return nil, fmt.Errorf("expected docId argument to be 'readme.txt', got %q", docId) } // If arguments weren't extracted, parse from URI as fallback From 8e85125ece3d517b7c589c5b2ed6b0b694f363f2 Mon Sep 17 00:00:00 2001 From: Slach Date: Thu, 26 Jun 2025 14:40:24 +0400 Subject: [PATCH 7/8] simplify TestServerWithResourceTemplate logic and increase test coverage --- mcptest/mcptest_test.go | 50 +++++++++++------------------------------ 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index b0971d72d..c8fb59def 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -194,63 +194,39 @@ func TestServerWithResourceTemplate(t *testing.T) { srv := mcptest.NewUnstartedServer(t) defer srv.Close() - // Create a URI template for files like "file://users/{userId}/documents/{docId}" - uriTemplate := &mcp.URITemplate{} - if err := uriTemplate.UnmarshalJSON([]byte(`"file://users/{userId}/documents/{docId}"`)); err != nil { - t.Fatal("URITemplate.UnmarshalJSON:", err) - } - - template := mcp.ResourceTemplate{ - URITemplate: uriTemplate, - Name: "User Document", - Description: "A user's document", - MIMEType: "text/plain", - } + template := mcp.NewResourceTemplate( + "file://users/{userId}/documents/{docId}", + "User Document", + mcp.WithTemplateDescription("A user's document"), + mcp.WithTemplateMIMEType("text/plain"), + ) handler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - // First verify the arguments were correctly extracted from the URI template if request.Params.Arguments == nil { return nil, fmt.Errorf("expected arguments to be populated from URI template") } - userId, ok := request.Params.Arguments["userId"].(string) + userIds, ok := request.Params.Arguments["userId"].([]string) if !ok { return nil, fmt.Errorf("expected userId argument to be populated from URI template") } - if userId != "john" { - return nil, fmt.Errorf("expected userId argument to be 'john', got %q", userId) + if len(userIds) != 1 && userIds[0] != "john" { + return nil, fmt.Errorf("expected userId argument to be 'john', got %v", userIds) } - docId, ok := request.Params.Arguments["docId"].(string) + docIds, ok := request.Params.Arguments["docId"].([]string) if !ok { return nil, fmt.Errorf("expected docId argument to be populated from URI template") } - if docId != "readme.txt" { - return nil, fmt.Errorf("expected docId argument to be 'readme.txt', got %q", docId) - } - - // If arguments weren't extracted, parse from URI as fallback - if userId == "" || docId == "" { - // Parse "file://users/john/documents/readme.txt" to extract john and readme.txt - uri := request.Params.URI - if len(uri) > 13 && uri[:13] == "file://users/" { - parts := uri[13:] // Remove "file://users/" - if idx := strings.Index(parts, "/documents/"); idx > 0 { - userId = parts[:idx] - docId = parts[idx+11:] // Remove "/documents/" - } - } - } - - if userId == "" || docId == "" { - return nil, fmt.Errorf("could not extract userId and docId from URI: %s", request.Params.URI) + if len(docIds) != 1 && docIds[0] != "readme.txt" { + return nil, fmt.Errorf("expected docId argument to be 'readme.txt', got %v", docIds) } return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: request.Params.URI, MIMEType: "text/plain", - Text: fmt.Sprintf("Document %s for user %s", docId, userId), + Text: fmt.Sprintf("Document %s for user %s", docIds[0], userIds[0]), }, }, nil } From 2f0e9a616b0ce989f327d58331f1449537071d3d Mon Sep 17 00:00:00 2001 From: Slach Date: Sat, 28 Jun 2025 17:12:56 +0400 Subject: [PATCH 8/8] changes after code review https://github.com/mark3labs/mcp-go/pull/449#discussion_r2173226222 --- mcptest/mcptest_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mcptest/mcptest_test.go b/mcptest/mcptest_test.go index c8fb59def..18922cb84 100644 --- a/mcptest/mcptest_test.go +++ b/mcptest/mcptest_test.go @@ -210,15 +210,21 @@ func TestServerWithResourceTemplate(t *testing.T) { if !ok { return nil, fmt.Errorf("expected userId argument to be populated from URI template") } - if len(userIds) != 1 && userIds[0] != "john" { - return nil, fmt.Errorf("expected userId argument to be 'john', got %v", userIds) + if len(userIds) != 1 { + return nil, fmt.Errorf("expected userId to have one value, but got %d", len(userIds)) + } + if userIds[0] != "john" { + return nil, fmt.Errorf("expected userId argument to be 'john', got %s", userIds[0]) } docIds, ok := request.Params.Arguments["docId"].([]string) if !ok { return nil, fmt.Errorf("expected docId argument to be populated from URI template") } - if len(docIds) != 1 && docIds[0] != "readme.txt" { + if len(docIds) != 1 { + return nil, fmt.Errorf("expected docId to have one value, but got %d", len(docIds)) + } + if docIds[0] != "readme.txt" { return nil, fmt.Errorf("expected docId argument to be 'readme.txt', got %v", docIds) }