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>
136 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|