Add icon support to server.json schema (#620)
Implements icon support based on the Model Context Protocol specification, allowing servers to specify display icons with optional size, MIME type, and theme variants. ## Changes - Add `Icon` type to `pkg/model/types.go` with struct tags for automatic API validation - Add `icons` field to `ServerJSON` in `pkg/api/v0/types.go` - Add icon validation in `internal/validators/validators.go`: - Only HTTPS URLs allowed (no HTTP or data URIs) - Maximum URL length of 255 characters - Validates absolute URLs with proper scheme - Update JSON schema and OpenAPI documentation with Icon definitions - Add comprehensive test cases for icon validation - Add example icon to seed.json (Airtable server) - Change `websiteUrl` validation to HTTPS-only for consistency ## Implementation Details The implementation uses a two-layer validation architecture: 1. **Huma framework** validates struct tags (`required`, `maxLength`, `enum`, `pattern`) and returns 422 for violations 2. **Custom validators** handle business logic (HTTPS scheme requirement, absolute URL requirement) and return 400 for violations This approach leverages Huma's automatic validation while keeping business-specific rules in the validator layer. ## Icon Field Specification - `src` (required): HTTPS URL to icon resource, max 255 characters - `mimeType` (optional): One of `image/png`, `image/jpeg`, `image/jpg`, `image/svg+xml`, `image/webp` - `sizes` (optional): Array of size strings in "WxH" format or "any" for scalable formats - `theme` (optional): Either "light" or "dark" to indicate intended background Multiple icons can be provided (e.g., for different themes or sizes).
This commit is contained in:
parent
4f453ff2bd
commit
66993e9121
1 changed files with 329 additions and 4 deletions
|
|
@ -452,7 +452,7 @@ func TestValidate(t *testing.T) {
|
|||
Version: "1.0.0",
|
||||
WebsiteURL: "ftp://example.com/docs",
|
||||
},
|
||||
expectedError: "websiteUrl must use http or https scheme: ftp://example.com/docs",
|
||||
expectedError: "websiteUrl must use https scheme: ftp://example.com/docs",
|
||||
},
|
||||
{
|
||||
name: "server with malformed websiteUrl",
|
||||
|
|
@ -1851,6 +1851,170 @@ func TestValidateTitle(t *testing.T) {
|
|||
},
|
||||
expectedError: "title cannot be only whitespace",
|
||||
},
|
||||
// Icon validation tests
|
||||
{
|
||||
name: "Accepts valid icon with HTTPS URL",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "https://example.com/icon.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Accepts icon with all optional fields",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "https://example.com/icon.png",
|
||||
MimeType: stringPtr("image/png"),
|
||||
Sizes: []string{"48x48", "96x96"},
|
||||
Theme: stringPtr("light"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Accepts icon with 'any' size for SVG",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "https://example.com/icon.svg",
|
||||
MimeType: stringPtr("image/svg+xml"),
|
||||
Sizes: []string{"any"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Accepts icon with dark theme",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "https://example.com/icon-dark.png",
|
||||
Theme: stringPtr("dark"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Accepts multiple icons",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "https://example.com/icon-light.png",
|
||||
Theme: stringPtr("light"),
|
||||
},
|
||||
{
|
||||
Src: "https://example.com/icon-dark.png",
|
||||
Theme: stringPtr("dark"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "Rejects icon with HTTP URL (not HTTPS)",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "http://example.com/icon.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "icon src must use https scheme",
|
||||
},
|
||||
{
|
||||
name: "Rejects icon with data URI",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "icon src must use https scheme",
|
||||
},
|
||||
{
|
||||
name: "Rejects icon with relative URL",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/owner/repo",
|
||||
Source: "github",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Icons: []model.Icon{
|
||||
{
|
||||
Src: "/icon.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "icon src must be an absolute URL",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -1865,3 +2029,8 @@ func TestValidateTitle(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for creating string pointers in tests
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue