Skip to content

Commit 8b9ac40

Browse files
authored
Adjust output of Registry API (#2001)
* removed other commits Signed-off-by: Daniele Martinoli <[email protected]> * added test target Signed-off-by: Daniele Martinoli <[email protected]> --------- Signed-off-by: Daniele Martinoli <[email protected]>
1 parent 42c2d6f commit 8b9ac40

File tree

3 files changed

+615
-23
lines changed

3 files changed

+615
-23
lines changed

cmd/thv-registry-api/Taskfile.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ tasks:
1616
desc: Build the registry API image with ko
1717
cmds:
1818
- ko build --local -B ./cmd/thv-registry-api
19+
20+
registry-test:
21+
desc: Run registry API tests
22+
dir: cmd/thv-registry-api
23+
cmds:
24+
- go test ./api/v1 -v

cmd/thv-registry-api/api/v1/routes.go

Lines changed: 214 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
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
4775
type 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

Comments
 (0)