Skip to content

Commit c3f3434

Browse files
committed
Merge branch 'main' of github.com:docker/mcp-registry into add-astro-docs-server
2 parents 790159a + a903d08 commit c3f3434

34 files changed

Lines changed: 2805 additions & 29 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,4 @@ jobs:
2929
3030
- name: Build and catalog changed servers
3131
shell: bash
32-
run: |
33-
set -eo pipefail
34-
while IFS= read -r file; do
35-
dir=$(dirname "$file")
36-
name=$(basename "$dir")
37-
task validate -- --name $name
38-
task build -- --tools --pull-community $name
39-
echo "--------------------------------"
40-
task catalog -- $name
41-
echo "--------------------------------"
42-
cat catalogs/$name/catalog.yaml
43-
echo "--------------------------------"
44-
done < changed-servers.txt
32+
run: ./scripts/ci-validation.sh

CONTRIBUTING.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ task create -- --category database --image myorg/my-mcp https://github.com/myorg
123123
After creating your server file with `task create`, you will be given instructions for running it locally. In the case of my-orgdb-mcp, we would run the following commands next.
124124

125125
```
126-
task build -- my-orgdb-mcp # Not needed if providing your own image
126+
task build -- --tools my-orgdb-mcp # Not needed if providing your own image
127127
task catalog -- my-orgdb-mcp
128128
docker mcp catalog import $PWD/catalogs/my-orgdb-mcp/catalog.yaml
129129
```
@@ -134,6 +134,43 @@ Now, if we go into the MCP Toolkit on Docker Desktop, we'll see our new MCP serv
134134
docker mcp catalog reset
135135
```
136136

137+
### Avoiding `build --tools` failures
138+
139+
If your MCP server needs to be configured before listing tools, you can now provide a `tools.json` file and the build process will not try to run
140+
the server and list the tools. This is one of the most common issues that block your PR.
141+
142+
This is an example of a `tools.json` file:
143+
144+
```
145+
[
146+
{
147+
"name": "tools_name",
148+
"description": "description of what you tool does"
149+
"arguments": [
150+
{
151+
"name": "name_of_the_argument",
152+
"type": "string",
153+
"desc": ""
154+
}
155+
]
156+
},
157+
{
158+
"name": "another_tool",
159+
"description": "description of what another tool"
160+
"arguments": [
161+
{
162+
"name": "name_of_the_argument",
163+
"type": "string",
164+
"desc": ""
165+
}
166+
]
167+
}
168+
]
169+
```
170+
171+
When this file is found next to your `server.yaml`, the `task build -- --tools your-server-name` lists the tools by reading the file instead of
172+
running the server.
173+
137174
### 4️⃣ Wait for review and approval
138175

139176
Upon approval your entry will be processed and it will be available in 24 hours at:

cmd/build/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ func run(ctx context.Context, name string, listTools bool, pullCommunity bool) e
4848
return nil
4949
}
5050

51+
if server.Type == "poci" {
52+
fmt.Printf("✅ Build skipped for poci server %s\n", name)
53+
return nil
54+
}
55+
5156
isMcpImage := strings.HasPrefix(server.Image, "mcp/")
5257

5358
if isMcpImage {
@@ -62,6 +67,16 @@ func run(ctx context.Context, name string, listTools bool, pullCommunity bool) e
6267
return err
6368
}
6469
}
70+
// check if the server has a tools.json file
71+
if _, err := os.Stat(filepath.Join("servers", name, "tools.json")); err == nil {
72+
listTools = false
73+
tools, err := mcp.ReadToolsFromFile(filepath.Join("servers", name, "tools.json"))
74+
if err != nil {
75+
return err
76+
}
77+
fmt.Println()
78+
fmt.Println(len(tools), "tools found.")
79+
}
6580

6681
if listTools {
6782
tools, err := mcp.Tools(ctx, server, false, false, false)

cmd/validate/main.go

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"flag"
67
"fmt"
78
"image"
89
"log"
910
"net/http"
1011
"os"
12+
"os/exec"
1113
"path/filepath"
1214
"regexp"
1315
"strings"
16+
"time"
1417

1518
_ "image/jpeg"
1619
_ "image/png"
1720

1821
"github.com/docker/mcp-registry/internal/licenses"
22+
"github.com/docker/mcp-registry/internal/mcp"
1923
"github.com/docker/mcp-registry/pkg/github"
2024
"github.com/docker/mcp-registry/pkg/servers"
2125
"gopkg.in/yaml.v3"
@@ -56,11 +60,15 @@ func run(name string) error {
5660
if err := isRemoteValid(name); err != nil {
5761
return err
5862
}
59-
63+
6064
if err := isOAuthDynamicValid(name); err != nil {
6165
return err
6266
}
6367

68+
if err := isPociValid(name); err != nil {
69+
return err
70+
}
71+
6472
return nil
6573
}
6674

@@ -147,13 +155,13 @@ func IsLicenseValid(name string) error {
147155
if err != nil {
148156
return err
149157
}
150-
158+
151159
// Skip license validation for remote servers without source
152160
if server.Source.Project == "" {
153161
fmt.Println("✅ License validation skipped (remote server)")
154162
return nil
155163
}
156-
164+
157165
repository, err := client.GetProjectRepository(ctx, server.Source.Project)
158166
if err != nil {
159167
return err
@@ -193,14 +201,14 @@ func isIconValid(name string) error {
193201
fmt.Println("🛑 Icon is too large. It must be less than 2MB")
194202
return nil
195203
}
196-
204+
197205
// Check content type for SVG support
198206
contentType := resp.Header.Get("Content-Type")
199207
if contentType == "image/svg+xml" {
200208
fmt.Println("✅ Icon is valid (SVG)")
201209
return nil
202210
}
203-
211+
204212
img, format, err := image.DecodeConfig(resp.Body)
205213
if err != nil {
206214
return err
@@ -250,10 +258,52 @@ func isRemoteValid(name string) error {
250258
return fmt.Errorf("remote server transport_type must be one of: stdio, sse, streamable-http (got: %s)", server.Remote.TransportType)
251259
}
252260

261+
if err := hasValidTools(server); err != nil {
262+
return err
263+
}
264+
253265
fmt.Println("✅ Remote is valid")
254266
return nil
255267
}
256268

269+
// Check that there is either a tools.json, dynamic tools, or can fetch remote tools
270+
func hasValidTools(server servers.Server) error {
271+
defaultErr := fmt.Errorf("server must have either a tools.json, dynamic tools, or can fetch remote tools")
272+
273+
// Dynamic tools are valid
274+
if server.Dynamic != nil && server.Dynamic.Tools {
275+
fmt.Println("✅ Dynamic tools are valid")
276+
return nil
277+
}
278+
279+
// Tools.json is valid
280+
tools, err := readToolsJson(server.Name)
281+
if err == nil {
282+
toolCount := len(tools)
283+
fmt.Printf("✅ tools.json is valid. Found %d tools.\n", toolCount)
284+
return nil
285+
}
286+
if !os.IsNotExist(err) {
287+
fmt.Printf("🛑 Tools.json could not be read: %v\n", err)
288+
return defaultErr
289+
}
290+
291+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
292+
defer cancel()
293+
294+
// Remote tools are valid
295+
remoteTools, err := mcp.RemoteTools(ctx, server)
296+
if err != nil {
297+
fmt.Printf("🛑 Remote tools could not be fetched (if auth is required, specify \ndynamic:\n tools: true\n): %v\n", err)
298+
return defaultErr
299+
}
300+
301+
toolCount := len(remoteTools)
302+
303+
fmt.Printf("✅ Remote tools are valid. Found %d tools.\n", toolCount)
304+
return nil
305+
}
306+
257307
// check if servers with OAuth have dynamic tools enabled
258308
func isOAuthDynamicValid(name string) error {
259309
server, err := readServerYaml(name)
@@ -284,3 +334,48 @@ func readServerYaml(name string) (servers.Server, error) {
284334
}
285335
return server, nil
286336
}
337+
338+
func readToolsJson(name string) ([]mcp.Tool, error) {
339+
path := filepath.Join("servers", name, "tools.json")
340+
buf, err := os.ReadFile(path)
341+
if err != nil {
342+
return nil, err
343+
}
344+
345+
var tools []mcp.Tool
346+
if err := json.Unmarshal(buf, &tools); err != nil {
347+
return nil, err
348+
}
349+
350+
return tools, nil
351+
}
352+
353+
func isPociValid(name string) error {
354+
server, err := readServerYaml(name)
355+
if err != nil {
356+
return err
357+
}
358+
359+
if server.Type != "poci" {
360+
return nil
361+
}
362+
363+
for _, tool := range server.Tools {
364+
if tool.Container.Image != "" {
365+
if err := pullPociImage(tool.Container.Image); err != nil {
366+
fmt.Printf("🛑 Could not pull poci image %s: %v\n", tool.Container.Image, err)
367+
return err
368+
}
369+
}
370+
}
371+
372+
fmt.Println("✅ Poci image is valid")
373+
return nil
374+
}
375+
376+
func pullPociImage(image string) error {
377+
cmd := exec.Command("docker", "pull", image)
378+
cmd.Stdout = os.Stdout
379+
cmd.Stderr = os.Stderr
380+
return cmd.Run()
381+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ require (
2424
github.com/dustin/go-humanize v1.0.1 // indirect
2525
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
2626
github.com/google/go-querystring v1.1.0 // indirect
27+
github.com/google/uuid v1.6.0 // indirect
2728
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2829
github.com/mattn/go-isatty v0.0.20 // indirect
2930
github.com/mattn/go-localereader v0.0.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHS
5353
github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY=
5454
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
5555
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
56+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
57+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5658
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
5759
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
5860
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=

0 commit comments

Comments
 (0)