@@ -16,6 +16,7 @@ import (
1616
1717 "github.com/speakeasy-api/gram/server/gen/types"
1818 "github.com/speakeasy-api/gram/server/internal/attr"
19+ "github.com/speakeasy-api/gram/server/internal/cache"
1920 externalmcptypes "github.com/speakeasy-api/gram/server/internal/externalmcp/repo/types"
2021 "github.com/speakeasy-api/gram/server/internal/o11y"
2122 "github.com/speakeasy-api/gram/server/internal/oops"
@@ -28,23 +29,46 @@ type RegistryBackend interface {
2829
2930// RegistryClient handles communication with external MCP registries.
3031type RegistryClient struct {
31- httpClient * http.Client
32- logger * slog.Logger
33- backend RegistryBackend
32+ httpClient * http.Client
33+ logger * slog.Logger
34+ backend RegistryBackend
35+ listCache * cache.TypedCacheObject [CachedListServersResponse ]
36+ detailsCache * cache.TypedCacheObject [CachedServerDetailsResponse ]
3437}
3538
36- // NewRegistryClient creates a new registry client.
37- func NewRegistryClient (logger * slog.Logger , tracerProvider trace.TracerProvider , backend RegistryBackend ) * RegistryClient {
38- return & RegistryClient {
39+ // NewRegistryClient creates a new registry client. The cacheImpl parameter is
40+ // optional — pass nil to disable caching.
41+ func NewRegistryClient (logger * slog.Logger , tracerProvider trace.TracerProvider , backend RegistryBackend , cacheImpl cache.Cache ) * RegistryClient {
42+ rc := & RegistryClient {
3943 httpClient : & http.Client {
4044 Transport : otelhttp .NewTransport (
4145 retryablehttp .NewClient ().StandardClient ().Transport ,
4246 otelhttp .WithTracerProvider (tracerProvider ),
4347 ),
4448 },
45- logger : logger .With (attr .SlogComponent ("mcp-registry-client" )),
46- backend : backend ,
49+ logger : logger .With (attr .SlogComponent ("mcp-registry-client" )),
50+ backend : backend ,
51+ listCache : nil ,
52+ detailsCache : nil ,
4753 }
54+
55+ if cacheImpl != nil {
56+ listCache := cache .NewTypedObjectCache [CachedListServersResponse ](
57+ logger .With (attr .SlogCacheNamespace ("registry-list" )),
58+ cacheImpl ,
59+ cache .SuffixNone ,
60+ )
61+ rc .listCache = & listCache
62+
63+ detailsCache := cache .NewTypedObjectCache [CachedServerDetailsResponse ](
64+ logger .With (attr .SlogCacheNamespace ("registry-details" )),
65+ cacheImpl ,
66+ cache .SuffixNone ,
67+ )
68+ rc .detailsCache = & detailsCache
69+ }
70+
71+ return rc
4872}
4973
5074// Registry represents an MCP registry endpoint.
@@ -173,6 +197,16 @@ func (c *RegistryClient) ListServers(ctx context.Context, registry Registry, par
173197 }
174198 }
175199
200+ // Check cache after authorization so headers are populated.
201+ if c .listCache != nil {
202+ cacheKey := registryCacheKey ("list" , req )
203+ cached , err := c .listCache .Get (ctx , cacheKey )
204+ if err == nil {
205+ c .logger .DebugContext (ctx , "registry list cache hit" , attr .SlogCacheKey (cacheKey ))
206+ return cached .Servers , nil
207+ }
208+ }
209+
176210 resp , err := c .httpClient .Do (req )
177211 if err != nil {
178212 return nil , fmt .Errorf ("failed to fetch from registry: %w" , err )
@@ -231,6 +265,17 @@ func (c *RegistryClient) ListServers(ctx context.Context, registry Registry, par
231265 servers = append (servers , server )
232266 }
233267
268+ // Store in cache on success.
269+ if c .listCache != nil {
270+ cacheKey := registryCacheKey ("list" , req )
271+ if storeErr := c .listCache .Store (ctx , CachedListServersResponse {
272+ Key : cacheKey ,
273+ Servers : servers ,
274+ }); storeErr != nil {
275+ c .logger .WarnContext (ctx , "failed to store registry list in cache" , attr .SlogError (storeErr ))
276+ }
277+ }
278+
234279 return servers , nil
235280}
236281
@@ -266,6 +311,16 @@ func (c *RegistryClient) GetServerDetails(ctx context.Context, registry Registry
266311 }
267312 }
268313
314+ // Check cache after authorization so headers are populated.
315+ if c .detailsCache != nil {
316+ cacheKey := registryCacheKey ("details" , req )
317+ cached , err := c .detailsCache .Get (ctx , cacheKey )
318+ if err == nil {
319+ c .logger .DebugContext (ctx , "registry details cache hit" , attr .SlogCacheKey (cacheKey ))
320+ return cached .Details , nil
321+ }
322+ }
323+
269324 resp , err := c .httpClient .Do (req )
270325 if err != nil {
271326 return nil , fmt .Errorf ("send external mcp server details request: %w" , err )
@@ -326,13 +381,26 @@ func (c *RegistryClient) GetServerDetails(ctx context.Context, registry Registry
326381 tools = serverResp .Meta .Version .FifthRemote .Tools
327382 }
328383
329- return & ServerDetails {
384+ details := & ServerDetails {
330385 Name : serverResp .Server .Name ,
331386 Description : serverResp .Server .Description ,
332387 Version : serverResp .Server .Version ,
333388 RemoteURL : remoteURL ,
334389 TransportType : transportType ,
335390 Tools : tools ,
336391 Headers : headers ,
337- }, nil
392+ }
393+
394+ // Store in cache on success.
395+ if c .detailsCache != nil {
396+ cacheKey := registryCacheKey ("details" , req )
397+ if storeErr := c .detailsCache .Store (ctx , CachedServerDetailsResponse {
398+ Key : cacheKey ,
399+ Details : details ,
400+ }); storeErr != nil {
401+ c .logger .WarnContext (ctx , "failed to store registry details in cache" , attr .SlogError (storeErr ))
402+ }
403+ }
404+
405+ return details , nil
338406}
0 commit comments