internal/mcp: implement resource templates
Provide support for resource templates on the client and server. Adding, removing and listing resource templates looks just like those operations on resources. Although the spec provides no guidance on the unique ID for resource templates, we follow most existing SDKs, as well as the parallel with resources, and index on the URI template rather than the name. As the spec recommends, the server verifies an incoming URI in a ReadResource request against its list of resources and resource templates, to make sure the URI came from one of them. The main technical challenge here is the need to check that a URI template could have produced a particular URI. I wrote a very simplified version of this check. At some point we should switch to github.com/yosida95/uritemplate/v3. Change-Id: I1e34dd01c8f2171123c68c045ad62f5e85c05e7c Reviewed-on: https://go-review.googlesource.com/c/tools/+/680158 Auto-Submit: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
parent
d9a7859c3b
commit
c1524fe934
1 changed files with 262 additions and 25 deletions
|
|
@ -13,6 +13,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
|
||||
|
|
@ -25,7 +26,14 @@ type ServerResource struct {
|
|||
Handler ResourceHandler
|
||||
}
|
||||
|
||||
// A ServerResourceTemplate associates a ResourceTemplate with its handler.
|
||||
type ServerResourceTemplate struct {
|
||||
ResourceTemplate *ResourceTemplate
|
||||
Handler ResourceHandler
|
||||
}
|
||||
|
||||
// A ResourceHandler is a function that reads a resource.
|
||||
// It will be called when the client calls [ClientSession.ReadResource].
|
||||
// If it cannot find the resource, it should return the result of calling [ResourceNotFoundError].
|
||||
type ResourceHandler func(context.Context, *ServerSession, *ReadResourceParams) (*ReadResourceResult, error)
|
||||
|
||||
|
|
@ -145,3 +153,69 @@ func fileRoot(root *Root) (_ string, err error) {
|
|||
}
|
||||
return fileRoot, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue