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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ server, _, err := client.Servers.Get(ctx, "ai.waystation/gmail", &mcp.ServerGetO
Version: "1.0.0",
})

// Get a hosted remote server that uses a slash-qualified registry name
server, _, err := client.Servers.Get(ctx, "com.xquik/mcp", &mcp.ServerGetOptions{
Version: "2.4.8",
})

// Get all versions of a server by name
servers, _, err := client.Servers.ListVersionsByName(ctx, "ai.waystation/gmail")

Expand Down
1 change: 1 addition & 0 deletions examples/get/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func main() {
fmt.Println("\nExamples:")
fmt.Println(" go run main.go ai.waystation/gmail")
fmt.Println(" go run main.go ai.waystation/gmail 1.0.0")
fmt.Println(" go run main.go com.xquik/mcp 2.4.8")
fmt.Println("\nTo see available servers, run:")
fmt.Println(" go run ../list/main.go")
os.Exit(1)
Expand Down
11 changes: 10 additions & 1 deletion mcp/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@ func (r *RateLimitError) Is(target error) bool {
r.Message == v.Message &&
r.Response.StatusCode == v.Response.StatusCode &&
r.Response.Request.Method == v.Response.Request.Method &&
sanitizeURL(r.Response.Request.URL) == sanitizeURL(v.Response.Request.URL)
sameSanitizedURL(r.Response.Request.URL, v.Response.Request.URL)
}

func sameSanitizedURL(left, right *url.URL) bool {
left = sanitizeURL(left)
right = sanitizeURL(right)
if left == nil || right == nil {
return left == nil && right == nil
}
return left.String() == right.String()
}

// CheckResponse checks the API response for errors, and returns them if present.
Expand Down
72 changes: 65 additions & 7 deletions mcp/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,10 @@ func TestRateLimitError_Is(t *testing.T) {
}

tests := []struct {
name string
target error
want bool
name string
receiver *RateLimitError
target error
want bool
}{
{
name: "identical values but different instances",
Expand All @@ -155,9 +156,44 @@ func TestRateLimitError_Is(t *testing.T) {
Response: sharedResponse,
Message: "API rate limit exceeded",
},
// Note: Due to how sanitizeURL works (creates a new pointer each time),
// this will return false even though the errors are logically identical.
// This tests the current behavior, not the ideal behavior.
want: true,
},
{
name: "different URL",
target: &RateLimitError{
Rate: Rate{
Limit: 100,
Remaining: 0,
Reset: resetTime,
},
Response: &http.Response{
StatusCode: http.StatusTooManyRequests,
Request: &http.Request{
Method: "GET",
URL: mustParseURL("https://api.example.com/v0.1/other"),
},
},
Message: "API rate limit exceeded",
},
want: false,
},
{
name: "both URLs nil",
receiver: rateLimitErrorWithURL(baseErr, nil),
target: &RateLimitError{
Rate: baseErr.Rate,
Response: responseWithURL(http.MethodGet, nil),
Message: "API rate limit exceeded",
},
want: true,
},
{
name: "only target URL nil",
target: &RateLimitError{
Rate: baseErr.Rate,
Response: responseWithURL(http.MethodGet, nil),
Message: "API rate limit exceeded",
},
want: false,
},
{
Expand Down Expand Up @@ -195,14 +231,36 @@ func TestRateLimitError_Is(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := baseErr.Is(tt.target)
receiver := tt.receiver
if receiver == nil {
receiver = baseErr
}
got := receiver.Is(tt.target)
if got != tt.want {
t.Errorf("RateLimitError.Is() = %v, want %v", got, tt.want)
}
})
}
}

func rateLimitErrorWithURL(base *RateLimitError, requestURL *url.URL) *RateLimitError {
return &RateLimitError{
Rate: base.Rate,
Response: responseWithURL(base.Response.Request.Method, requestURL),
Message: base.Message,
}
}

func responseWithURL(method string, requestURL *url.URL) *http.Response {
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Request: &http.Request{
Method: method,
URL: requestURL,
},
}
}

func TestSanitizeURL(t *testing.T) {
tests := []struct {
name string
Expand Down
69 changes: 67 additions & 2 deletions mcp/servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,67 @@ func TestServersService_Get(t *testing.T) {
},
expectError: false,
},
{
name: "successful get hosted remote with source repository",
serverName: "com.xquik/mcp",
opts: &ServerGetOptions{Version: "2.4.8"},
Comment thread
kriptoburak marked this conversation as resolved.
statusCode: http.StatusOK,
responseBody: `{
"server": {
"name": "com.xquik/mcp",
"version": "2.4.8",
"description": "X data platform with REST endpoints, webhooks, monitoring, giveaway draws, and MCP tools.",
"repository": {
"url": "https://github.com/Xquik-dev/x-twitter-scraper",
"source": "github"
},
"remotes": [
{
"type": "streamable-http",
"url": "https://xquik.com/mcp",
"headers": [
{
"name": "Authorization",
"isSecret": true
}
]
}
]
},
"_meta": {
"io.modelcontextprotocol.registry/official": {
"status": "active",
"publishedAt": "2026-07-04T00:00:00Z",
"updatedAt": "2026-07-04T00:00:00Z",
"isLatest": true
}
}
}`,
expectedResult: &registryv0.ServerJSON{
Name: "com.xquik/mcp",
Version: "2.4.8",
Description: "X data platform with REST endpoints, webhooks, monitoring, giveaway draws, and MCP tools.",
Repository: model.Repository{
URL: "https://github.com/Xquik-dev/x-twitter-scraper",
Source: "github",
},
Remotes: []model.Transport{
{
Type: "streamable-http",
URL: "https://xquik.com/mcp",
Headers: []model.KeyValueInput{
{
Name: "Authorization",
InputWithVariables: model.InputWithVariables{
Input: model.Input{IsSecret: true},
},
},
},
},
},
},
expectError: false,
},
}

for _, tt := range tests {
Expand All @@ -250,11 +311,15 @@ func TestServersService_Get(t *testing.T) {
version = url.PathEscape(tt.opts.Version)
}

mux.HandleFunc(fmt.Sprintf("/v0.1/servers/%s/versions/%s", url.PathEscape(tt.serverName), version), func(w http.ResponseWriter, r *http.Request) {
expectedPath := fmt.Sprintf("/v0.1/servers/%s/versions/%s", url.PathEscape(tt.serverName), version)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
if r.URL.EscapedPath() != expectedPath {
t.Errorf("Request path = %q, want %q", r.URL.EscapedPath(), expectedPath)
}

w.WriteHeader(tt.statusCode)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(tt.statusCode)
fmt.Fprint(w, tt.responseBody)
})

Expand Down