Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ paths:
schema:
type: string
example: "1.2.3"
- name: type
in: query
description: |
Filter by distribution type. Allows discovering servers based on how they are made available to users.

Example values include: 'remote', 'npm', 'pypi', 'oci', 'nuget', 'mcpb'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth changing remote to sse and streamable-http? I know that at Anthropic we maybe have plans to deprecate sse so being able to filter by the actual remote type might be useful - and it also aligns with filtering by the package type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's reasonable 👍 Would it be a problem if we continue to have remote though as an option that aggregates all remote servers?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a slight preference for making this composable in some way, e.g. you can specify the type multiple times or you can comma-separate types or something? then we don't need remote, you can just specify streamable-http,sse


Note: Servers with multiple package types or both remotes and packages will appear in results for all matching type filters.
required: false
schema:
type: string
example: "remote"
responses:
'200':
description: A list of MCP servers
Expand Down
13 changes: 13 additions & 0 deletions internal/api/handlers/v0/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/service"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
"github.com/modelcontextprotocol/registry/pkg/model"
)

const errRecordNotFound = "record not found"
Expand All @@ -23,6 +24,7 @@ type ListServersInput struct {
UpdatedSince string `query:"updated_since" doc:"Filter servers updated since timestamp (RFC3339 datetime)" required:"false" example:"2025-08-07T13:15:04.280Z"`
Search string `query:"search" doc:"Search servers by name (substring match)" required:"false" example:"filesystem"`
Version string `query:"version" doc:"Filter by version ('latest' for latest version, or an exact version like '1.2.3')" required:"false" example:"latest"`
Type string `query:"type" doc:"Filter by distribution type: 'remote' for remote transports, or package type ('npm', 'pypi', 'oci', 'nuget', 'mcpb')" required:"false" example:"remote"`
}

// ServerDetailInput represents the input for getting server details
Expand All @@ -42,6 +44,8 @@ type ServerVersionsInput struct {
}

// RegisterServersEndpoints registers all server-related endpoints with a custom path prefix
//
//nolint:cyclop // Function registers multiple endpoints with validation logic
func RegisterServersEndpoints(api huma.API, pathPrefix string, registry service.RegistryService) {
// List servers endpoint
huma.Register(api, huma.Operation{
Expand Down Expand Up @@ -82,6 +86,15 @@ func RegisterServersEndpoints(api huma.API, pathPrefix string, registry service.
}
}

// Handle type parameter
if input.Type != "" {
// Validate that the type is a valid distribution type
if !model.IsValidDistributionType(input.Type) {
return nil, huma.Error400BadRequest("Invalid type parameter: must be one of 'remote', 'npm', 'pypi', 'oci', 'nuget', or 'mcpb'")
}
filter.ConfigType = &input.Type
}

// Get paginated results with filtering
servers, nextCursor, err := registry.ListServers(ctx, filter, input.Cursor, input.Limit)
if err != nil {
Expand Down
277 changes: 277 additions & 0 deletions internal/api/handlers/v0/servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,280 @@ func TestServersEndpointEdgeCases(t *testing.T) {
}
})
}

func TestListServersWithTypeFilter(t *testing.T) {
ctx := context.Background()
registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig())

// Setup test data with different distribution types

// Server with remote only
_, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/remote-only",
Description: "Server with remote transport only",
Version: "1.0.0",
Remotes: []model.Transport{
{
Type: "streamable-http",
URL: "https://example.com/mcp",
},
},
})
require.NoError(t, err)

// Server with npm package
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/npm-server",
Description: "Server with npm package",
Version: "1.0.0",
Packages: []model.Package{
{
RegistryType: "npm",
Identifier: "@example/mcp-server",
Version: "1.0.0",
Transport: model.Transport{
Type: "stdio",
},
},
},
})
require.NoError(t, err)

// Server with pypi package
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/pypi-server",
Description: "Server with pypi package",
Version: "1.0.0",
Packages: []model.Package{
{
RegistryType: "pypi",
Identifier: "example-mcp-server",
Version: "1.0.0",
Transport: model.Transport{
Type: "stdio",
},
},
},
})
require.NoError(t, err)

// Server with OCI package
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/oci-server",
Description: "Server with OCI container",
Version: "1.0.0",
Packages: []model.Package{
{
RegistryType: "oci",
Identifier: "ghcr.io/example/mcp-server:1.0.0",
Transport: model.Transport{
Type: "stdio",
},
},
},
})
require.NoError(t, err)

// Server with NuGet package
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/nuget-server",
Description: "Server with NuGet package",
Version: "1.0.0",
Packages: []model.Package{
{
RegistryType: "nuget",
Identifier: "Example.MCP.Server",
Version: "1.0.0",
Transport: model.Transport{
Type: "stdio",
},
},
},
})
require.NoError(t, err)

// Server with MCPB package
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/mcpb-server",
Description: "Server with MCPB package",
Version: "1.0.0",
Packages: []model.Package{
{
RegistryType: "mcpb",
Identifier: "https://example.com/server.mcpb",
FileSHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
Transport: model.Transport{
Type: "stdio",
},
},
},
})
require.NoError(t, err)

// Server with both remote and package
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/mixed-server",
Description: "Server with both remote and package",
Version: "1.0.0",
Remotes: []model.Transport{
{
Type: "sse",
URL: "https://example.com/sse",
},
},
Packages: []model.Package{
{
RegistryType: "npm",
Identifier: "@example/mixed-server",
Version: "1.0.0",
Transport: model.Transport{
Type: "stdio",
},
},
},
})
require.NoError(t, err)

// Server with multiple package types
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
Schema: model.CurrentSchemaURL,
Name: "com.example/multi-package",
Description: "Server with multiple package types",
Version: "1.0.0",
Packages: []model.Package{
{
RegistryType: "npm",
Identifier: "@example/multi-server",
Version: "1.0.0",
Transport: model.Transport{
Type: "stdio",
},
},
{
RegistryType: "pypi",
Identifier: "example-multi-server",
Version: "1.0.0",
Transport: model.Transport{
Type: "stdio",
},
},
},
})
require.NoError(t, err)

// Create API
mux := http.NewServeMux()
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
v0.RegisterServersEndpoints(api, "/v0", registryService)

tests := []struct {
name string
queryParams string
expectedStatus int
expectedCount int
expectedError string
expectedServers []string
}{
{
name: "filter by remote type",
queryParams: "?type=remote",
expectedStatus: http.StatusOK,
expectedCount: 2,
expectedServers: []string{"com.example/remote-only", "com.example/mixed-server"},
},
{
name: "filter by npm type",
queryParams: "?type=npm",
expectedStatus: http.StatusOK,
expectedCount: 3,
expectedServers: []string{"com.example/npm-server", "com.example/mixed-server", "com.example/multi-package"},
},
{
name: "filter by pypi type",
queryParams: "?type=pypi",
expectedStatus: http.StatusOK,
expectedCount: 2,
expectedServers: []string{"com.example/pypi-server", "com.example/multi-package"},
},
{
name: "filter by oci type",
queryParams: "?type=oci",
expectedStatus: http.StatusOK,
expectedCount: 1,
expectedServers: []string{"com.example/oci-server"},
},
{
name: "filter by nuget type",
queryParams: "?type=nuget",
expectedStatus: http.StatusOK,
expectedCount: 1,
expectedServers: []string{"com.example/nuget-server"},
},
{
name: "filter by mcpb type",
queryParams: "?type=mcpb",
expectedStatus: http.StatusOK,
expectedCount: 1,
expectedServers: []string{"com.example/mcpb-server"},
},
{
name: "invalid type parameter",
queryParams: "?type=invalid",
expectedStatus: http.StatusBadRequest,
expectedError: "Invalid type parameter",
},
{
name: "combine type with search",
queryParams: "?type=npm&search=npm",
expectedStatus: http.StatusOK,
expectedCount: 1,
expectedServers: []string{"com.example/npm-server"},
},
{
name: "combine type with version",
queryParams: "?type=remote&version=latest",
expectedStatus: http.StatusOK,
expectedCount: 2,
expectedServers: []string{"com.example/remote-only", "com.example/mixed-server"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v0/servers"+tt.queryParams, nil)
w := httptest.NewRecorder()

mux.ServeHTTP(w, req)

assert.Equal(t, tt.expectedStatus, w.Code)

if tt.expectedStatus == http.StatusOK {
var resp apiv0.ServerListResponse
err := json.NewDecoder(w.Body).Decode(&resp)
assert.NoError(t, err)
assert.Len(t, resp.Servers, tt.expectedCount)
assert.Equal(t, tt.expectedCount, resp.Metadata.Count)

// Verify the expected servers are returned
if tt.expectedServers != nil {
returnedServers := make(map[string]bool)
for _, server := range resp.Servers {
returnedServers[server.Server.Name] = true
}
for _, expectedServer := range tt.expectedServers {
assert.True(t, returnedServers[expectedServer], "Expected server %s to be in results", expectedServer)
}
}
} else if tt.expectedError != "" {
assert.Contains(t, w.Body.String(), tt.expectedError)
}
})
}
}
1 change: 1 addition & 0 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type ServerFilter struct {
SubstringName *string // for substring search on name
Version *string // for exact version matching
IsLatest *bool // for filtering latest versions only
ConfigType *string // for filtering by distribution type: "remote", "npm", "pypi", "oci", "nuget", or "mcpb"
}

// Database defines the interface for database operations
Expand Down
16 changes: 16 additions & 0 deletions internal/database/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func NewPostgreSQL(ctx context.Context, connectionURI string) (*PostgreSQL, erro
}, nil
}

//nolint:cyclop // Function builds complex SQL query with many optional filters
func (db *PostgreSQL) ListServers(
ctx context.Context,
tx pgx.Tx,
Expand Down Expand Up @@ -131,6 +132,21 @@ func (db *PostgreSQL) ListServers(
args = append(args, *filter.IsLatest)
argIndex++
}
if filter.ConfigType != nil {
switch *filter.ConfigType {
case string(model.DistributionRemote):
// Check if remotes array has at least one element
whereConditions = append(whereConditions, "jsonb_array_length(COALESCE(value->'remotes', '[]'::jsonb)) > 0")
case string(model.DistributionNpm), string(model.DistributionPypi), string(model.DistributionOci), string(model.DistributionNuget), string(model.DistributionMcpb):
// Check if packages array contains at least one package with the specified registryType
whereConditions = append(whereConditions, fmt.Sprintf(
"EXISTS (SELECT 1 FROM jsonb_array_elements(value->'packages') AS pkg WHERE pkg->>'registryType' = $%d)",
argIndex,
))
args = append(args, *filter.ConfigType)
argIndex++
}
}
}

// Add cursor pagination using compound serverName:version cursor
Expand Down
Loading
Loading