go-sdk/mcp/resource_test.go
Jonathan Amsterdam c1524fe934 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>
2025-06-09 10:59:01 -07:00

136 lines
3.5 KiB
Go

// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestFileRoot(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO: fix for Windows")
}
for _, tt := range []struct {
uri string
want string
wantErr string // error must contain this string
}{
{uri: "file:///foo", want: "/foo"},
{uri: "file:///foo/bar", want: "/foo/bar"},
{uri: "file:///foo/../bar", want: "/bar"},
{uri: "file:/foo", want: "/foo"},
{uri: "http:///foo", wantErr: "not a file"},
{uri: "file://foo", wantErr: "empty path"},
{uri: ":", wantErr: "missing protocol scheme"},
} {
got, err := fileRoot(&Root{URI: tt.uri})
if err != nil {
if tt.wantErr == "" {
t.Errorf("%s: got %v, want success", tt.uri, err)
continue
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("%s: got %v, does not contain %q", tt.uri, err, tt.wantErr)
continue
}
} else if tt.wantErr != "" {
t.Errorf("%s: succeeded, but wanted error with %q", tt.uri, tt.wantErr)
} else if got != tt.want {
t.Errorf("%s: got %q, want %q", tt.uri, got, tt.want)
}
}
}
func TestComputeURIFilepath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO: fix for Windows")
}
// TODO(jba): test with Windows \\host paths and C: paths
dirFilepath := filepath.FromSlash("/files")
rootFilepaths := []string{
filepath.FromSlash("/files/public"),
filepath.FromSlash("/files/shared"),
}
for _, tt := range []struct {
uri string
want string
wantErr string // error must contain this
}{
{"file:///public", "public", ""},
{"file:///public/file", "public/file", ""},
{"file:///shared/file", "shared/file", ""},
{"http:///foo", "", "not a file"},
{"file://foo", "", "empty"},
{"file://foo/../bar", "", "localized"},
{"file:///secret", "", "root"},
{"file:///secret/file", "", "root"},
{"file:///private/file", "", "root"},
} {
t.Run(tt.uri, func(t *testing.T) {
tt.want = filepath.FromSlash(tt.want) // handle Windows
got, gotErr := computeURIFilepath(tt.uri, dirFilepath, rootFilepaths)
if gotErr != nil {
if tt.wantErr == "" {
t.Fatalf("got %v, wanted success", gotErr)
}
if !strings.Contains(gotErr.Error(), tt.wantErr) {
t.Fatalf("got error %v, does not contain %q", gotErr, tt.wantErr)
}
return
}
if tt.wantErr != "" {
t.Fatal("succeeded unexpectedly")
}
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
func TestReadFileResource(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO: fix for Windows")
}
abs, err := filepath.Abs("testdata")
if err != nil {
t.Fatal(err)
}
dirFilepath := filepath.Join(abs, "files")
got, err := readFileResource("file:///info.txt", dirFilepath, nil)
if err != nil {
t.Fatal(err)
}
want := "Contents\n"
if g := string(got); g != want {
t.Errorf("got %q, want %q", g, want)
}
}
func TestTemplateMatch(t *testing.T) {
uri := "file:///path/to/file"
for _, tt := range []struct {
template string
want bool
}{
{"file:///{}/{a}/{b}", true},
{"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)
}
}
}