diff --git a/mcp/server.go b/mcp/server.go index d118464a..348c06e7 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -22,6 +22,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" "github.com/modelcontextprotocol/go-sdk/internal/util" "github.com/modelcontextprotocol/go-sdk/jsonrpc" + "github.com/yosida95/uritemplate/v3" ) const DefaultPageSize = 1000 @@ -229,7 +230,19 @@ func (s *Server) RemoveResources(uris ...string) { func (s *Server) AddResourceTemplate(t *ResourceTemplate, h ResourceHandler) { s.changeAndNotify(notificationResourceListChanged, &ResourceListChangedParams{}, func() bool { - // TODO: check template validity. + // Validate the URI template syntax + _, err := uritemplate.New(t.URITemplate) + if err != nil { + panic(fmt.Errorf("URI template %q is invalid: %w", t.URITemplate, err)) + } + // Ensure the URI template has a valid scheme + u, err := url.Parse(t.URITemplate) + if err != nil { + panic(err) // url.Parse includes the URI in the error + } + if !u.IsAbs() { + panic(fmt.Errorf("URI template %q needs a scheme", t.URITemplate)) + } s.resourceTemplates.add(&serverResourceTemplate{t, h}) return true }) diff --git a/mcp/server_test.go b/mcp/server_test.go index adadc9c3..39a4cdb4 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -373,6 +373,42 @@ func TestServerCapabilities(t *testing.T) { } } +func TestServerAddResourceTemplate(t *testing.T) { + tests := []struct { + name string + template string + expectPanic bool + }{ + {"ValidFileTemplate", "file:///{a}/{b}", false}, + {"ValidCustomScheme", "myproto:///{a}", false}, + {"MissingScheme1", "://example.com/{path}", true}, + {"MissingScheme2", "/api/v1/users/{id}", true}, + {"EmptyVariable", "file:///{}/{b}", true}, + {"UnclosedVariable", "file:///{a", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rt := ResourceTemplate{URITemplate: tt.template} + + defer func() { + if r := recover(); r != nil { + if !tt.expectPanic { + t.Errorf("%s: unexpected panic: %v", tt.name, r) + } + } else { + if tt.expectPanic { + t.Errorf("%s: expected panic but did not panic", tt.name) + } + } + }() + + s := NewServer(testImpl, nil) + s.AddResourceTemplate(&rt, nil) + }) + } +} + // TestServerSessionkeepaliveCancelOverwritten is to verify that `ServerSession.keepaliveCancel` is assigned exactly once, // ensuring that only a single goroutine is responsible for the session's keepalive ping mechanism. func TestServerSessionkeepaliveCancelOverwritten(t *testing.T) {