From acde2579af6e0242e0bb4aba13a00893e5810d05 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sat, 4 Jul 2026 15:01:02 +0300 Subject: [PATCH 1/3] test: add Xquik registry lookup coverage --- README.md | 5 ++++ examples/get/main.go | 1 + mcp/servers_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/README.md b/README.md index 4e36552..088419c 100644 --- a/README.md +++ b/README.md @@ -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") diff --git a/examples/get/main.go b/examples/get/main.go index 0476c1f..e775301 100644 --- a/examples/get/main.go +++ b/examples/get/main.go @@ -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) diff --git a/mcp/servers_test.go b/mcp/servers_test.go index 08130b7..875f018 100644 --- a/mcp/servers_test.go +++ b/mcp/servers_test.go @@ -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"}, + 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: ®istryv0.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 { From 50575db90133f5653bd287c89f2f19f3f02b2362 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 19 Jul 2026 06:05:16 +0300 Subject: [PATCH 2/3] fix: validate escaped registry paths and rate errors --- mcp/errors.go | 2 +- mcp/errors_test.go | 22 +++++++++++++++++++--- mcp/servers_test.go | 8 ++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/mcp/errors.go b/mcp/errors.go index 352666f..3451a08 100644 --- a/mcp/errors.go +++ b/mcp/errors.go @@ -66,7 +66,7 @@ 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) + sanitizeURL(r.Response.Request.URL).String() == sanitizeURL(v.Response.Request.URL).String() } // CheckResponse checks the API response for errors, and returns them if present. diff --git a/mcp/errors_test.go b/mcp/errors_test.go index 96c0b5c..57f0805 100644 --- a/mcp/errors_test.go +++ b/mcp/errors_test.go @@ -155,9 +155,25 @@ 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, }, { diff --git a/mcp/servers_test.go b/mcp/servers_test.go index 875f018..9483333 100644 --- a/mcp/servers_test.go +++ b/mcp/servers_test.go @@ -311,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) }) From 9d9e4e19114b5e03bcf870d5a8ae3ca5ec047f40 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 19 Jul 2026 06:09:42 +0300 Subject: [PATCH 3/3] fix: compare rate-limit URLs safely --- mcp/errors.go | 11 +++++++++- mcp/errors_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/mcp/errors.go b/mcp/errors.go index 3451a08..c6a030e 100644 --- a/mcp/errors.go +++ b/mcp/errors.go @@ -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).String() == sanitizeURL(v.Response.Request.URL).String() + 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. diff --git a/mcp/errors_test.go b/mcp/errors_test.go index 57f0805..4a14fb9 100644 --- a/mcp/errors_test.go +++ b/mcp/errors_test.go @@ -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", @@ -176,6 +177,25 @@ func TestRateLimitError_Is(t *testing.T) { }, 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, + }, { name: "different rate", target: &RateLimitError{ @@ -211,7 +231,11 @@ 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) } @@ -219,6 +243,24 @@ func TestRateLimitError_Is(t *testing.T) { } } +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