mcp/Resource: Refactor URI Template Matching with uritemplate Library (#100)

1. Replaced the `uriTemplateToRegexp` function with the use of
[yosida95/uritemplate](https://github.com/yosida95/uritemplate) to
handle URI template matching as per the request.
2. Updated the relevant test cases to verify the new implementation and
fixed an invalid URI template (`"file:///{}/{a}/{b}"`) that doesn't
conform to RFC 6570 due to an empty `{}` expression.
3. Updated `go.mod` and `go.sum` to reflect the new dependency.

Fixes #11.
This commit is contained in:
cryo 2025-07-09 03:52:54 +08:00 • committed by GitHub
commit fbff31af14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 10 additions and 68 deletions

1
go.mod
View file

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

2
go.sum
View file

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

View file

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

View file

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