Skip to content

Commit 8a73c30

Browse files
authored
Feat add validate cmd (#80)
* feat: add command to validate servers Signed-off-by: Ivan Pedrazas <[email protected]>
1 parent 5f1c6cc commit 8a73c30

File tree

3 files changed

+290
-0
lines changed

3 files changed

+290
-0
lines changed

Taskfile.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ tasks:
1717
desc: Run the wizard
1818
cmd: go run ./cmd/wizard {{.CLI_ARGS}}
1919

20+
validate:
21+
desc: Validate a server
22+
cmd: go run ./cmd/validate {{.CLI_ARGS}}
23+
2024
import:
2125
desc: Import a server into the registry
2226
cmd: docker mcp catalog import ./catalogs/{{.CLI_ARGS}}/catalog.yaml

cmd/validate/main.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"image"
8+
"log"
9+
"net/http"
10+
"os"
11+
"path/filepath"
12+
"regexp"
13+
"strings"
14+
15+
_ "image/jpeg"
16+
_ "image/png"
17+
18+
"github.com/docker/mcp-registry/internal/licenses"
19+
"github.com/docker/mcp-registry/pkg/github"
20+
"github.com/docker/mcp-registry/pkg/servers"
21+
"gopkg.in/yaml.v3"
22+
)
23+
24+
func main() {
25+
name := flag.String("name", "", "Name of the mcp server, name is guessed if not provided")
26+
flag.Parse()
27+
28+
if err := run(*name); err != nil {
29+
log.Fatal(err)
30+
}
31+
}
32+
33+
func run(name string) error {
34+
if err := isNameValid(name); err != nil {
35+
return err
36+
}
37+
38+
if err := isDirectoryValid(name); err != nil {
39+
return err
40+
}
41+
42+
if err := areSecretsValid(name); err != nil {
43+
return err
44+
}
45+
46+
if err := IsLicenseValid(name); err != nil {
47+
return err
48+
}
49+
if err := isIconValid(name); err != nil {
50+
return err
51+
}
52+
53+
return nil
54+
}
55+
56+
// check if the name is a valid
57+
func isNameValid(name string) error {
58+
// check if name has only letters, numbers, and hyphens
59+
if !regexp.MustCompile(`^[a-z0-9-]+$`).MatchString(name) {
60+
return fmt.Errorf("name is not valid. It must be a lowercase string with only letters, numbers, and hyphens")
61+
}
62+
63+
fmt.Println("✅ Name is valid")
64+
return nil
65+
}
66+
67+
// check if the directory is valid
68+
// servers/<NAME>/server.yaml exists
69+
func isDirectoryValid(name string) error {
70+
_, err := os.Stat(filepath.Join("servers", name, "server.yaml"))
71+
if err != nil {
72+
return err
73+
}
74+
server, err := readServerYaml(name)
75+
if err != nil {
76+
return err
77+
}
78+
79+
// check if the server.yaml file has a valid name
80+
if server.Name != name {
81+
return fmt.Errorf("server.yaml file has a invalid name. It must be %s", name)
82+
}
83+
84+
fmt.Println("✅ Directory is valid")
85+
return nil
86+
}
87+
88+
// check if the secrets are valid
89+
// secrets must be prefixed with the name of the server
90+
func areSecretsValid(name string) error {
91+
// read the server.yaml file
92+
server, err := readServerYaml(name)
93+
if err != nil {
94+
return err
95+
}
96+
97+
// check if the server.yaml file has a valid secrets
98+
if len(server.Config.Secrets) > 0 {
99+
for _, secret := range server.Config.Secrets {
100+
if !strings.HasPrefix(secret.Name, name+".") {
101+
return fmt.Errorf("secret %s is not valid. It must be prefixed with the name of the server", secret.Name)
102+
}
103+
}
104+
}
105+
106+
fmt.Println("✅ Secrets are valid")
107+
return nil
108+
}
109+
110+
// check if the license is valid
111+
// the license must be valid
112+
func IsLicenseValid(name string) error {
113+
ctx := context.Background()
114+
client := github.New()
115+
server, err := readServerYaml(name)
116+
if err != nil {
117+
return err
118+
}
119+
repository, err := client.GetProjectRepository(ctx, server.Source.Project)
120+
if err != nil {
121+
return err
122+
}
123+
124+
if !licenses.IsValid(repository.License) {
125+
return fmt.Errorf("project %s is licensed under %s which may be incompatible with some tools", server.Source.Project, repository.License.GetName())
126+
}
127+
fmt.Println("✅ License is valid")
128+
129+
return nil
130+
}
131+
132+
func isIconValid(name string) error {
133+
server, err := readServerYaml(name)
134+
if err != nil {
135+
return err
136+
}
137+
138+
if server.Image == "" {
139+
return fmt.Errorf("image is not valid. It must be a valid image")
140+
}
141+
// fetch the image and check the size
142+
resp, err := http.Get(server.About.Icon)
143+
if err != nil {
144+
return err
145+
}
146+
defer resp.Body.Close()
147+
if resp.StatusCode != 200 {
148+
return fmt.Errorf("image is not valid. It must be a valid image")
149+
}
150+
if resp.ContentLength > 2*1024*1024 {
151+
return fmt.Errorf("image is too large. It must be less than 2MB")
152+
}
153+
img, format, err := image.DecodeConfig(resp.Body)
154+
if err != nil {
155+
return err
156+
}
157+
if format != "png" {
158+
return fmt.Errorf("image is not a png. It must be a png")
159+
}
160+
161+
if img.Width > 512 || img.Height > 512 {
162+
return fmt.Errorf("image is too large. It must be less than 512x512")
163+
}
164+
165+
fmt.Println("✅ Icon is valid")
166+
return nil
167+
}
168+
169+
func readServerYaml(name string) (servers.Server, error) {
170+
serverYaml, err := os.ReadFile(filepath.Join("servers", name, "server.yaml"))
171+
if err != nil {
172+
return servers.Server{}, err
173+
}
174+
var server servers.Server
175+
err = yaml.Unmarshal(serverYaml, &server)
176+
if err != nil {
177+
return servers.Server{}, err
178+
}
179+
return server, nil
180+
}

cmd/validate/main_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func Test_isNameValid(t *testing.T) {
8+
type args struct {
9+
name string
10+
}
11+
tests := []struct {
12+
name string
13+
args args
14+
wantError bool
15+
}{
16+
{
17+
name: "valid name",
18+
args: args{
19+
name: "my-server",
20+
},
21+
wantError: false,
22+
},
23+
{
24+
name: "invalid name",
25+
args: args{
26+
name: "My-Server",
27+
},
28+
wantError: true,
29+
},
30+
{
31+
name: "valid name with numbers",
32+
args: args{
33+
name: "my-server-1",
34+
},
35+
wantError: false,
36+
},
37+
{
38+
name: "invalid name with symbol",
39+
args: args{
40+
name: "my-server-$",
41+
},
42+
wantError: true,
43+
},
44+
{
45+
name: "invalid name with space",
46+
args: args{
47+
name: "my server",
48+
},
49+
wantError: true,
50+
},
51+
{
52+
name: "invalid name with slash",
53+
args: args{
54+
name: "my-server/1",
55+
},
56+
wantError: true,
57+
},
58+
}
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
if got := isNameValid(tt.args.name); (got != nil) != tt.wantError {
62+
t.Errorf("isNameValid() = %v, want %v", got, tt.wantError)
63+
}
64+
})
65+
}
66+
}
67+
68+
func Test_areSecretsValid(t *testing.T) {
69+
type args struct {
70+
name string
71+
}
72+
tests := []struct {
73+
name string
74+
args args
75+
wantError bool
76+
}{
77+
{
78+
name: "valid secrets",
79+
args: args{
80+
name: "astra-db",
81+
},
82+
wantError: false,
83+
},
84+
{
85+
name: "no secrets",
86+
args: args{
87+
name: "arxiv-mcp-server",
88+
},
89+
wantError: false,
90+
},
91+
{
92+
name: "invalid secrets",
93+
args: args{
94+
name: "bad-server",
95+
},
96+
wantError: true,
97+
},
98+
}
99+
for _, tt := range tests {
100+
t.Run(tt.name, func(t *testing.T) {
101+
if got := areSecretsValid(tt.args.name); (got != nil) != tt.wantError {
102+
t.Errorf("areSecretsValid() = %v, want %v", got, tt.wantError)
103+
}
104+
})
105+
}
106+
}

0 commit comments

Comments
 (0)