返回 DeepSeek-Reasonix
registry_test.go
根目录 / internal / mcpregistry / registry_test.go
1 package mcpregistry
2
3 import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "os"
9 "path/filepath"
10 "testing"
11 "time"
12 )
13
14 func TestSearchNormalizesOfficialRegistryEntries(t *testing.T) {
15 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16 if r.URL.Path != "/v0.1/servers" || r.URL.Query().Get("version") != "latest" || r.URL.Query().Get("search") != "demo" {
17 t.Fatalf("request = %s", r.URL)
18 }
19 _ = json.NewEncoder(w).Encode(map[string]any{"servers": []any{
20 map[string]any{"server": map[string]any{
21 "name": "io.example/remote", "title": "Remote", "version": "1.2.0",
22 "remotes": []any{map[string]any{"type": "streamable-http", "url": "https://mcp.example/mcp"}},
23 }},
24 map[string]any{"server": map[string]any{
25 "name": "io.example/package", "description": "Package server", "version": "2.0.0",
26 "packages": []any{map[string]any{
27 "registryType": "npm", "identifier": "@example/mcp", "version": "2.0.0",
28 "transport": map[string]any{"type": "stdio"},
29 }},
30 }},
31 map[string]any{"server": map[string]any{
32 "name": "io.example/manual", "version": "1.0.0",
33 "remotes": []any{map[string]any{
34 "type": "streamable-http", "url": "https://mcp.example/{tenant}",
35 "variables": map[string]any{"tenant": map[string]any{"isRequired": true}},
36 }},
37 }},
38 }})
39 }))
40 defer server.Close()
41
42 client := New(filepath.Join(t.TempDir(), "registry.json"))
43 client.BaseURL = server.URL
44 result, err := client.Search(context.Background(), "demo", 10)
45 if err != nil {
46 t.Fatal(err)
47 }
48 if result.Cached || len(result.Entries) != 3 {
49 t.Fatalf("result = %+v", result)
50 }
51 remote := result.Entries[0]
52 if !remote.Installable || remote.Transport != "http" || remote.URL != "https://mcp.example/mcp" {
53 t.Fatalf("remote = %+v", remote)
54 }
55 pkg := result.Entries[1]
56 if !pkg.Installable || pkg.Transport != "stdio" || pkg.Command != "npx" || len(pkg.Args) != 2 || pkg.Args[1] != "@example/mcp@2.0.0" {
57 t.Fatalf("package = %+v", pkg)
58 }
59 if result.Entries[2].Installable || result.Entries[2].UnavailableReason == "" {
60 t.Fatalf("manual entry = %+v", result.Entries[2])
61 }
62 entry, err := pkg.PluginEntry("")
63 if err != nil || entry.Name != "package" || entry.Command != "npx" {
64 t.Fatalf("PluginEntry = %+v, %v", entry, err)
65 }
66 }
67
68 func TestSearchFallsBackToMatchingCache(t *testing.T) {
69 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
70 _ = json.NewEncoder(w).Encode(map[string]any{"servers": []any{map[string]any{"server": map[string]any{
71 "name": "io.example/cached", "version": "1", "remotes": []any{map[string]any{"type": "sse", "url": "https://mcp.example/sse"}},
72 }}}})
73 }))
74 cachePath := filepath.Join(t.TempDir(), "registry.json")
75 client := New(cachePath)
76 client.BaseURL = server.URL
77 client.Now = func() time.Time { return time.Unix(1_000_000, 0) }
78 if _, err := client.Search(context.Background(), "cached", 5); err != nil {
79 t.Fatal(err)
80 }
81 server.Close()
82 client.HTTP = &http.Client{Timeout: 100 * time.Millisecond}
83 result, err := client.Search(context.Background(), "cached", 5)
84 if err != nil {
85 t.Fatal(err)
86 }
87 if !result.Cached || result.Warning == "" || len(result.Entries) != 1 || result.Entries[0].Transport != "sse" {
88 t.Fatalf("cached result = %+v", result)
89 }
90 }
91
92 func TestSearchExpiresEachCachedQueryIndependently(t *testing.T) {
93 now := time.Unix(1_000_000, 0)
94 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
95 query := r.URL.Query().Get("search")
96 _ = json.NewEncoder(w).Encode(map[string]any{"servers": []any{map[string]any{"server": map[string]any{
97 "name": "io.example/" + query,
98 "remotes": []any{map[string]any{"type": "sse", "url": "https://mcp.example/" + query}},
99 }}}})
100 }))
101 cachePath := filepath.Join(t.TempDir(), "registry.json")
102 client := New(cachePath)
103 client.BaseURL = server.URL
104 client.Now = func() time.Time { return now }
105 if _, err := client.Search(context.Background(), "old", 5); err != nil {
106 t.Fatal(err)
107 }
108 now = now.Add(maxCacheAge + time.Hour)
109 if _, err := client.Search(context.Background(), "fresh", 5); err != nil {
110 t.Fatal(err)
111 }
112 server.Close()
113 client.HTTP = &http.Client{Timeout: 100 * time.Millisecond}
114
115 if _, err := client.Search(context.Background(), "old", 5); err == nil {
116 t.Fatal("expired query reused after an unrelated query refreshed the cache")
117 }
118 result, err := client.Search(context.Background(), "fresh", 5)
119 if err != nil {
120 t.Fatal(err)
121 }
122 if !result.Cached || len(result.Entries) != 1 || result.Entries[0].Name != "io.example/fresh" {
123 t.Fatalf("fresh cached result = %+v", result)
124 }
125 }
126
127 func TestSearchReadsLegacyGlobalTimestampCache(t *testing.T) {
128 now := time.Unix(1_000_000, 0)
129 key := cacheKey("legacy", 5)
130 data, err := json.Marshal(cacheFile{
131 FetchedAt: now,
132 Queries: map[string][]Entry{
133 key: {{Name: "io.example/legacy", Transport: "sse", URL: "https://mcp.example/legacy"}},
134 },
135 })
136 if err != nil {
137 t.Fatal(err)
138 }
139 cachePath := filepath.Join(t.TempDir(), "registry.json")
140 if err := os.WriteFile(cachePath, data, 0o600); err != nil {
141 t.Fatal(err)
142 }
143 client := New(cachePath)
144 client.BaseURL = "http://127.0.0.1:1"
145 client.HTTP = &http.Client{Timeout: 100 * time.Millisecond}
146 client.Now = func() time.Time { return now.Add(time.Hour) }
147 result, err := client.Search(context.Background(), "legacy", 5)
148 if err != nil {
149 t.Fatal(err)
150 }
151 if !result.Cached || len(result.Entries) != 1 || result.Entries[0].Name != "io.example/legacy" {
152 t.Fatalf("legacy cached result = %+v", result)
153 }
154 }
155
156 func TestResolveRequiresLiveRegistryMetadata(t *testing.T) {
157 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
158 _ = json.NewEncoder(w).Encode(map[string]any{"servers": []any{map[string]any{"server": map[string]any{
159 "name": "io.example/demo",
160 "remotes": []any{map[string]any{"type": "streamable-http", "url": "https://mcp.example/demo"}},
161 }}}})
162 }))
163 client := New(filepath.Join(t.TempDir(), "registry.json"))
164 client.BaseURL = server.URL
165 if _, err := client.Search(context.Background(), "io.example/demo", maxLimit); err != nil {
166 t.Fatal(err)
167 }
168 server.Close()
169 client.HTTP = &http.Client{Timeout: 100 * time.Millisecond}
170
171 if _, result, err := client.Resolve(context.Background(), "io.example/demo"); err == nil {
172 t.Fatalf("Resolve used cached install metadata: %+v", result)
173 }
174 }
175
176 func TestSuggestedName(t *testing.T) {
177 if got := SuggestedName("io.github.Example/My MCP Server"); got != "my-mcp-server" {
178 t.Fatalf("SuggestedName = %q", got)
179 }
180 }
181
181 lines GO