Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion mcp/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug"
Expand Down Expand Up @@ -186,13 +187,28 @@ func fileRoot(root *Root) (_ string, err error) {
return "", errors.New("empty path")
}
// We don't want Localize here: we want an absolute path, which is not local.
fileRoot := filepath.Clean(filepath.FromSlash(rurl.Path))
fileRoot := filepath.Clean(filepath.FromSlash(fileURIPath(rurl.Path, runtime.GOOS)))
if !filepath.IsAbs(fileRoot) {
return "", errors.New("not an absolute path")
}
return fileRoot, nil
}

// fileURIPath returns the path of a file URI in the form used by goos.
// On Windows, the path of a file URI for a drive letter path starts with a
// slash, as in file:///C:/dir (RFC 8089, Appendix E.2), which is dropped so
// that the result is the absolute path C:/dir.
func fileURIPath(uriPath, goos string) string {
if goos == "windows" && len(uriPath) >= 3 && uriPath[0] == '/' && uriPath[2] == ':' && isASCIILetter(uriPath[1]) {
return uriPath[1:]
}
return uriPath
}

func isASCIILetter(c byte) bool {
return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'
}

// Matches reports whether the receiver's uri template matches the uri.
func (sr *serverResourceTemplate) Matches(uri string) bool {
tmpl, err := uritemplate.New(sr.resourceTemplate.URITemplate)
Expand Down
52 changes: 41 additions & 11 deletions mcp/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,35 @@ import (
)

func TestFileRoot(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO: fix for Windows")
}

for _, tt := range []struct {
type test 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"},
}
tests := []test{
{uri: "http:///foo", wantErr: "not a file"},
{uri: "file://foo", wantErr: "empty path"},
{uri: ":", wantErr: "missing protocol scheme"},
} {
}
if runtime.GOOS == "windows" {
tests = append(tests,
test{uri: "file:///C:/foo", want: `C:\foo`},
test{uri: "file:///c:/foo/bar", want: `c:\foo\bar`},
test{uri: "file:///C:/foo/../bar", want: `C:\bar`},
test{uri: "file:/C:/foo", want: `C:\foo`},
// VS Code percent-encodes the colon after the drive letter.
test{uri: "file:///c%3A/foo", want: `c:\foo`},
test{uri: "file:///foo", wantErr: "not an absolute path"},
)
} else {
tests = append(tests,
test{uri: "file:///foo", want: "/foo"},
test{uri: "file:///foo/bar", want: "/foo/bar"},
test{uri: "file:///foo/../bar", want: "/bar"},
test{uri: "file:/foo", want: "/foo"},
)
}
for _, tt := range tests {
got, err := fileRoot(&Root{URI: tt.uri})
if err != nil {
if tt.wantErr == "" {
Expand All @@ -47,6 +59,24 @@ func TestFileRoot(t *testing.T) {
}
}

func TestFileURIPath(t *testing.T) {
for _, tt := range []struct {
uriPath, goos, want string
}{
{"/C:/foo", "windows", "C:/foo"},
{"/c:/foo", "windows", "c:/foo"},
{"/C:", "windows", "C:"},
{"/foo", "windows", "/foo"},
{"/1:/foo", "windows", "/1:/foo"},
{"/C:/foo", "linux", "/C:/foo"},
{"/foo", "linux", "/foo"},
} {
if got := fileURIPath(tt.uriPath, tt.goos); got != tt.want {
t.Errorf("fileURIPath(%q, %q) = %q, want %q", tt.uriPath, tt.goos, got, tt.want)
}
}
}

func TestComputeURIFilepath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO: fix for Windows")
Expand Down
Loading