55 "encoding/json"
66 "errors"
77 "net/http"
8+ "reflect"
89
910 "github.com/go-chi/chi/v5"
1011 "gopkg.in/yaml.v3"
@@ -33,20 +34,47 @@ type RegistryInfoResponse struct {
3334 TotalServers int `json:"total_servers"`
3435}
3536
36- // ServerResponse represents a server in API responses
37- type ServerResponse struct {
38- Name string `json:"name"`
39- Description string `json:"description"`
40- Tier string `json:"tier"`
41- Status string `json:"status"`
42- Transport string `json:"transport"`
43- Tools []string `json:"tools"`
37+ // ServerSummaryResponse represents a server in list API responses (summary view)
38+ type ServerSummaryResponse struct {
39+ Name string `json:"name"`
40+ Description string `json:"description"`
41+ Tier string `json:"tier"`
42+ Status string `json:"status"`
43+ Transport string `json:"transport"`
44+ ToolsCount int `json:"tools_count"`
45+ }
46+
47+ // EnvVarDetail represents detailed environment variable information
48+ type EnvVarDetail struct {
49+ Name string `json:"name"`
50+ Description string `json:"description"`
51+ Required bool `json:"required"`
52+ Default string `json:"default,omitempty"`
53+ Secret bool `json:"secret,omitempty"`
54+ }
55+
56+ // ServerDetailResponse represents a server in detail API responses (full view)
57+ type ServerDetailResponse struct {
58+ Name string `json:"name"`
59+ Description string `json:"description"`
60+ Tier string `json:"tier"`
61+ Status string `json:"status"`
62+ Transport string `json:"transport"`
63+ Tools []string `json:"tools"`
64+ EnvVars []EnvVarDetail `json:"env_vars,omitempty"`
65+ Permissions map [string ]interface {} `json:"permissions,omitempty"`
66+ Metadata map [string ]interface {} `json:"metadata,omitempty"`
67+ RepositoryURL string `json:"repository_url,omitempty"`
68+ Tags []string `json:"tags,omitempty"`
69+ Args []string `json:"args,omitempty"`
70+ Volumes map [string ]interface {} `json:"volumes,omitempty"`
71+ Image string `json:"image,omitempty"`
4472}
4573
4674// ListServersResponse represents the servers list response
4775type ListServersResponse struct {
48- Servers []ServerResponse `json:"servers"`
49- Total int `json:"total"`
76+ Servers []ServerSummaryResponse `json:"servers"`
77+ Total int `json:"total"`
5078}
5179
5280// ErrorResponse represents a standardized error response
@@ -73,7 +101,6 @@ func Router(svc service.RegistryService) http.Handler {
73101 r := chi .NewRouter ()
74102
75103 // MCP Registry API v0 compatible endpoints
76- r .Get ("/servers" , routes .listServers )
77104 r .Post ("/publish" , routes .publishServer )
78105
79106 // Registry metadata
@@ -185,10 +212,10 @@ func (rr *Routes) listServers(w http.ResponseWriter, r *http.Request) {
185212 return
186213 }
187214
188- // Convert to response format
189- serverResponses := make ([]ServerResponse , len (servers ))
215+ // Convert to summary response format
216+ serverResponses := make ([]ServerSummaryResponse , len (servers ))
190217 for i := range servers {
191- serverResponses [i ] = newServerResponse (servers [i ])
218+ serverResponses [i ] = newServerSummaryResponse (servers [i ])
192219 }
193220
194221 // Toolhive format response
@@ -209,7 +236,7 @@ func (rr *Routes) listServers(w http.ResponseWriter, r *http.Request) {
209236// @Produce json
210237// @Param name path string true "Server name"
211238// @Param format query string false "Response format" Enums(toolhive,upstream) default(toolhive)
212- // @Success 200 {object} ServerResponse
239+ // @Success 200 {object} ServerDetailResponse
213240// @Failure 400 {object} ErrorResponse
214241// @Failure 404 {object} ErrorResponse
215242// @Failure 501 {object} ErrorResponse
@@ -250,8 +277,8 @@ func (rr *Routes) getServer(w http.ResponseWriter, r *http.Request) {
250277 return
251278 }
252279
253- // Convert to response format
254- serverResponse := newServerResponse (server )
280+ // Convert to detailed response format
281+ serverResponse := newServerDetailResponse (server )
255282
256283 // Toolhive format
257284 rr .writeJSONResponse (w , serverResponse )
@@ -291,16 +318,170 @@ func (rr *Routes) listDeployedServers(w http.ResponseWriter, r *http.Request) {
291318 rr .writeJSONResponse (w , servers )
292319}
293320
294- // newServerResponse creates a ServerResponse from server metadata
295- func newServerResponse (server registry.ServerMetadata ) ServerResponse {
296- return ServerResponse {
321+ // newServerSummaryResponse creates a ServerSummaryResponse from server metadata
322+ func newServerSummaryResponse (server registry.ServerMetadata ) ServerSummaryResponse {
323+ return ServerSummaryResponse {
297324 Name : server .GetName (),
298325 Description : server .GetDescription (),
299326 Tier : server .GetTier (),
300327 Status : server .GetStatus (),
301328 Transport : server .GetTransport (),
302- Tools : server .GetTools (),
329+ ToolsCount : len (server .GetTools ()),
330+ }
331+ }
332+
333+ // newServerDetailResponse creates a ServerDetailResponse from server metadata with all available fields
334+ func newServerDetailResponse (server registry.ServerMetadata ) ServerDetailResponse {
335+ response := ServerDetailResponse {
336+ Name : server .GetName (),
337+ Description : server .GetDescription (),
338+ Tier : server .GetTier (),
339+ Status : server .GetStatus (),
340+ Transport : server .GetTransport (),
341+ Tools : server .GetTools (),
342+ RepositoryURL : server .GetRepositoryURL (),
343+ Tags : server .GetTags (),
344+ }
345+
346+ populateEnvVars (& response , server )
347+ populateMetadata (& response , server )
348+ populateServerTypeSpecificFields (& response , server )
349+
350+ return response
351+ }
352+
353+ // populateEnvVars converts and populates environment variables in the response
354+ func populateEnvVars (response * ServerDetailResponse , server registry.ServerMetadata ) {
355+ envVars := server .GetEnvVars ()
356+ if envVars == nil {
357+ return
358+ }
359+
360+ response .EnvVars = make ([]EnvVarDetail , 0 , len (envVars ))
361+ for _ , envVar := range envVars {
362+ if envVar != nil {
363+ response .EnvVars = append (response .EnvVars , EnvVarDetail {
364+ Name : envVar .Name ,
365+ Description : envVar .Description ,
366+ Required : envVar .Required ,
367+ Default : envVar .Default ,
368+ Secret : envVar .Secret ,
369+ })
370+ }
371+ }
372+ }
373+
374+ // populateMetadata converts and populates metadata in the response
375+ func populateMetadata (response * ServerDetailResponse , server registry.ServerMetadata ) {
376+ // Convert metadata from *Metadata to map[string]interface{}
377+ if metadata := server .GetMetadata (); metadata != nil {
378+ response .Metadata = map [string ]interface {}{
379+ "stars" : metadata .Stars ,
380+ "pulls" : metadata .Pulls ,
381+ "last_updated" : metadata .LastUpdated ,
382+ }
383+ }
384+
385+ // Add custom metadata
386+ if customMetadata := server .GetCustomMetadata (); customMetadata != nil {
387+ if response .Metadata == nil {
388+ response .Metadata = make (map [string ]interface {})
389+ }
390+ for k , v := range customMetadata {
391+ response .Metadata [k ] = v
392+ }
393+ }
394+ }
395+
396+ // populateServerTypeSpecificFields populates fields specific to container or remote servers
397+ func populateServerTypeSpecificFields (response * ServerDetailResponse , server registry.ServerMetadata ) {
398+ if ! server .IsRemote () {
399+ populateContainerServerFields (response , server )
400+ } else {
401+ populateRemoteServerFields (response , server )
402+ }
403+ }
404+
405+ // populateContainerServerFields populates fields specific to container servers (ImageMetadata)
406+ func populateContainerServerFields (response * ServerDetailResponse , server registry.ServerMetadata ) {
407+ // The server might be wrapped in a serverWithName struct from the service layer
408+ actualServer := extractEmbeddedServerMetadata (server )
409+
410+ // Type assert to access ImageMetadata-specific fields
411+ imgMetadata , ok := actualServer .(* registry.ImageMetadata )
412+ if ! ok {
413+ return
414+ }
415+
416+ // Add permissions if available
417+ if imgMetadata .Permissions != nil {
418+ response .Permissions = map [string ]interface {}{
419+ "profile" : imgMetadata .Permissions ,
420+ }
421+ }
422+
423+ // Add args if available
424+ if imgMetadata .Args != nil {
425+ response .Args = imgMetadata .Args
426+ }
427+
428+ // Add image as top-level field
429+ response .Image = imgMetadata .Image
430+
431+ // Add image-specific metadata
432+ if response .Metadata == nil {
433+ response .Metadata = make (map [string ]interface {})
303434 }
435+ response .Metadata ["target_port" ] = imgMetadata .TargetPort
436+ response .Metadata ["docker_tags" ] = imgMetadata .DockerTags
437+ }
438+
439+ // populateRemoteServerFields populates fields specific to remote servers
440+ func populateRemoteServerFields (response * ServerDetailResponse , server registry.ServerMetadata ) {
441+ // The server might be wrapped in a serverWithName struct from the service layer
442+ actualServer := extractEmbeddedServerMetadata (server )
443+
444+ remoteMetadata , ok := actualServer .(* registry.RemoteServerMetadata )
445+ if ! ok {
446+ return
447+ }
448+
449+ if response .Metadata == nil {
450+ response .Metadata = make (map [string ]interface {})
451+ }
452+
453+ response .Metadata ["url" ] = remoteMetadata .URL
454+ if remoteMetadata .Headers != nil {
455+ response .Metadata ["headers_count" ] = len (remoteMetadata .Headers )
456+ }
457+ response .Metadata ["oauth_enabled" ] = remoteMetadata .OAuthConfig != nil
458+ }
459+
460+ // extractEmbeddedServerMetadata extracts the embedded ServerMetadata from serverWithName wrapper
461+ func extractEmbeddedServerMetadata (server registry.ServerMetadata ) registry.ServerMetadata {
462+ // Use reflection to check if this is a struct with an embedded ServerMetadata field
463+ v := reflect .ValueOf (server )
464+ if v .Kind () == reflect .Ptr {
465+ v = v .Elem ()
466+ }
467+
468+ if v .Kind () == reflect .Struct {
469+ // Look for an embedded field of type registry.ServerMetadata
470+ for i := 0 ; i < v .NumField (); i ++ {
471+ field := v .Field (i )
472+ fieldType := v .Type ().Field (i )
473+
474+ // Check if it's an embedded field (Anonymous) that implements ServerMetadata
475+ if fieldType .Anonymous && field .CanInterface () {
476+ if serverMetadata , ok := field .Interface ().(registry.ServerMetadata ); ok {
477+ return serverMetadata
478+ }
479+ }
480+ }
481+ }
482+
483+ // If not wrapped, return the original server
484+ return server
304485}
305486
306487// getDeployedServer handles GET /api/v1/registry/servers/deployed/{name}
@@ -464,3 +645,15 @@ func serveOpenAPIYAML(w http.ResponseWriter, _ *http.Request) {
464645 w .WriteHeader (http .StatusOK )
465646 _ , _ = w .Write (yamlData )
466647}
648+
649+ // Test helpers - these functions are exported only for testing purposes
650+
651+ // NewServerSummaryResponseForTesting creates a ServerSummaryResponse for testing
652+ func NewServerSummaryResponseForTesting (server registry.ServerMetadata ) ServerSummaryResponse {
653+ return newServerSummaryResponse (server )
654+ }
655+
656+ // NewServerDetailResponseForTesting creates a ServerDetailResponse for testing
657+ func NewServerDetailResponseForTesting (server registry.ServerMetadata ) ServerDetailResponse {
658+ return newServerDetailResponse (server )
659+ }
0 commit comments