From ca1dd25ffd5c8f01f7bec5c21d24dff6aef81dce Mon Sep 17 00:00:00 2001 From: cryo Date: Tue, 8 Jul 2025 02:11:27 +0000 Subject: [PATCH] Refactor URI Template Matching with uritemplate Library --- go.mod | 1 + go.sum | 2 ++ mcp/resource.go | 63 +++----------------------------------------- mcp/resource_test.go | 12 +++------ 4 files changed, 10 insertions(+), 68 deletions(-) diff --git a/go.mod b/go.mod index 24e187ab..9bf8c151 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.23.0 require ( github.com/google/go-cmp v0.7.0 + github.com/yosida95/uritemplate/v3 v3.0.2 golang.org/x/tools v0.34.0 ) diff --git a/go.sum b/go.sum index 6c6c2a5d..7d2f581d 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= diff --git a/mcp/resource.go b/mcp/resource.go index 4202fdac..590e0672 100644 --- a/mcp/resource.go +++ b/mcp/resource.go @@ -13,11 +13,11 @@ import ( "net/url" "os" "path/filepath" - "regexp" "strings" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" "github.com/modelcontextprotocol/go-sdk/internal/util" + "github.com/yosida95/uritemplate/v3" ) // A serverResource associates a Resource with its handler. @@ -155,67 +155,10 @@ func fileRoot(root *Root) (_ string, err error) { } // Matches reports whether the receiver's uri template matches the uri. -// TODO: use "github.com/yosida95/uritemplate/v3" func (sr *serverResourceTemplate) Matches(uri string) bool { - re, err := uriTemplateToRegexp(sr.resourceTemplate.URITemplate) + tmpl, err := uritemplate.New(sr.resourceTemplate.URITemplate) if err != nil { return false } - return re.MatchString(uri) -} - -func uriTemplateToRegexp(uriTemplate string) (*regexp.Regexp, error) { - pat := uriTemplate - var b strings.Builder - b.WriteByte('^') - seen := map[string]bool{} - for len(pat) > 0 { - literal, rest, ok := strings.Cut(pat, "{") - b.WriteString(regexp.QuoteMeta(literal)) - if !ok { - break - } - expr, rest, ok := strings.Cut(rest, "}") - if !ok { - return nil, errors.New("missing '}'") - } - pat = rest - if strings.ContainsRune(expr, ',') { - return nil, errors.New("can't handle commas in expressions") - } - if strings.ContainsRune(expr, ':') { - return nil, errors.New("can't handle prefix modifiers in expressions") - } - if len(expr) > 0 && expr[len(expr)-1] == '*' { - return nil, errors.New("can't handle explode modifiers in expressions") - } - - // These sets of valid characters aren't accurate. - // See https://datatracker.ietf.org/doc/html/rfc6570. - var re, name string - first := byte(0) - if len(expr) > 0 { - first = expr[0] - } - switch first { - default: - // {var} doesn't match slashes. (It should also fail to match other characters, - // but this simplified implementation doesn't handle that.) - re = `[^/]*` - name = expr - case '+': - // {+var} matches anything, even slashes - re = `.*` - name = expr[1:] - case '#', '.', '/', ';', '?', '&': - return nil, fmt.Errorf("prefix character %c unsupported", first) - } - if seen[name] { - return nil, fmt.Errorf("can't handle duplicate name %q", name) - } - seen[name] = true - b.WriteString(re) - } - b.WriteByte('$') - return regexp.Compile(b.String()) + return tmpl.Regexp().MatchString(uri) } diff --git a/mcp/resource_test.go b/mcp/resource_test.go index cb5e4fb9..74d82f12 100644 --- a/mcp/resource_test.go +++ b/mcp/resource_test.go @@ -119,18 +119,14 @@ func TestTemplateMatch(t *testing.T) { template string want bool }{ - {"file:///{}/{a}/{b}", true}, + {"file:///{}/{a}/{b}", false}, // invalid: empty variable expression "{}" is not allowed in RFC 6570 {"file:///{a}/{b}", false}, {"file:///{+path}", true}, {"file:///{a}/{+path}", true}, } { - re, err := uriTemplateToRegexp(tt.template) - if err != nil { - t.Fatalf("%s: %v", tt.template, err) - } - got := re.MatchString(uri) - if got != tt.want { - t.Errorf("%s: got %t, want %t", tt.template, got, tt.want) + resourceTmpl := serverResourceTemplate{resourceTemplate: &ResourceTemplate{URITemplate: tt.template}} + if matched := resourceTmpl.Matches(uri); matched != tt.want { + t.Errorf("%s: got %t, want %t", tt.template, matched, tt.want) } } }