Skip to content

Commit cd0118e

Browse files
authored
Update inspect command to handle catalog entries which specify tools: dynamic true (#196)
* Update inspect command to handle catalog entries with dynamic tools
1 parent 7b384e9 commit cd0118e

File tree

3 files changed

+183
-3
lines changed

3 files changed

+183
-3
lines changed

cmd/docker-mcp/catalog/models.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ type Registry struct {
55
}
66

77
type Tile struct {
8-
Description string `yaml:"description"`
9-
ReadmeURL string `yaml:"readme"`
10-
ToolsURL string `yaml:"toolsUrl"`
8+
Description string `yaml:"description"`
9+
ReadmeURL string `yaml:"readme"`
10+
ToolsURL string `yaml:"toolsUrl"`
11+
Dynamic *DynamicFlags `yaml:"dynamic,omitempty"`
12+
}
13+
14+
type DynamicFlags struct {
15+
Tools bool `yaml:"tools"`
1116
}

cmd/docker-mcp/server/inspect.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ func Inspect(ctx context.Context, dockerClient docker.Client, serverName string)
6161
errs errgroup.Group
6262
)
6363
errs.Go(func() error {
64+
// Do not fetch tools if config states tools will be dynamic
65+
if server.Dynamic != nil && server.Dynamic.Tools {
66+
tools = []Tool{}
67+
return nil
68+
}
69+
6470
toolsRaw, err := fetch(ctx, server.ToolsURL)
6571
if err != nil {
6672
return err
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/docker/mcp-gateway/cmd/docker-mcp/catalog"
16+
"github.com/docker/mcp-gateway/pkg/docker"
17+
)
18+
19+
// TestInspectDynamicToolsServer tests that servers with dynamic tools
20+
// (dynamic: { tools: true }) are handled correctly without attempting to fetch tools
21+
func TestInspectDynamicToolsServer(t *testing.T) {
22+
ctx, home, dockerClient := setupInspectTest(t)
23+
24+
// Create mock HTTP server for readme
25+
readmeContent := "# Notion Remote\n\nThis is a remote server with dynamic tools."
26+
27+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
switch r.URL.Path {
29+
case "/readme.md":
30+
w.Header().Set("Content-Type", "text/markdown")
31+
_, _ = w.Write([]byte(readmeContent))
32+
default:
33+
http.NotFound(w, r)
34+
}
35+
}))
36+
defer server.Close()
37+
38+
// Create catalog with a server that has dynamic tools
39+
catalogYAML := `registry:
40+
notion-remote:
41+
description: Remote Notion server with dynamic tools
42+
title: Notion Remote
43+
type: remote
44+
dynamic:
45+
tools: true
46+
readme: ` + server.URL + `/readme.md
47+
`
48+
writeCatalogFile(t, home, catalogYAML)
49+
50+
// Inspect should succeed without attempting HTTP fetch for tools
51+
info, err := Inspect(ctx, dockerClient, "notion-remote")
52+
require.NoError(t, err, "Inspect should succeed for dynamic tools server")
53+
54+
// Dynamic tools servers should return empty tools array at inspect time
55+
assert.Empty(t, info.Tools, "Dynamic tools server should return empty tools array")
56+
57+
// Should have fetched readme
58+
assert.Equal(t, readmeContent, info.Readme)
59+
}
60+
61+
// TestInspectStaticToolsServer tests that servers with static toolsUrl
62+
// correctly fetch and parse tools
63+
func TestInspectStaticToolsServer(t *testing.T) {
64+
ctx, home, dockerClient := setupInspectTest(t)
65+
66+
// Create mock HTTP server for tools and readme
67+
toolsResponse := []Tool{
68+
{
69+
Name: "test-tool-1",
70+
Description: "First test tool",
71+
Enabled: true,
72+
},
73+
{
74+
Name: "test-tool-2",
75+
Description: "Second test tool",
76+
Enabled: true,
77+
},
78+
}
79+
toolsJSON, err := json.Marshal(toolsResponse)
80+
require.NoError(t, err)
81+
82+
readmeContent := "# Test Server\n\nThis is a test server."
83+
84+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85+
switch r.URL.Path {
86+
case "/tools.json":
87+
w.Header().Set("Content-Type", "application/json")
88+
_, _ = w.Write(toolsJSON)
89+
case "/readme.md":
90+
w.Header().Set("Content-Type", "text/markdown")
91+
_, _ = w.Write([]byte(readmeContent))
92+
default:
93+
http.NotFound(w, r)
94+
}
95+
}))
96+
defer server.Close()
97+
98+
// Create catalog with a server that has static toolsUrl
99+
catalogYAML := `registry:
100+
notion:
101+
description: Static Notion server
102+
title: Notion
103+
type: server
104+
readme: ` + server.URL + `/readme.md
105+
toolsUrl: ` + server.URL + `/tools.json
106+
`
107+
writeCatalogFile(t, home, catalogYAML)
108+
109+
// Inspect should successfully fetch and parse tools
110+
info, err := Inspect(ctx, dockerClient, "notion")
111+
require.NoError(t, err, "Inspect should succeed for static tools server")
112+
113+
// Should have fetched tools from the mock server
114+
assert.Len(t, info.Tools, 2, "Should have 2 tools")
115+
assert.Equal(t, "test-tool-1", info.Tools[0].Name)
116+
assert.Equal(t, "test-tool-2", info.Tools[1].Name)
117+
118+
// Should have fetched readme
119+
assert.Equal(t, readmeContent, info.Readme)
120+
}
121+
122+
// Test helpers
123+
124+
func setupInspectTest(t *testing.T) (context.Context, string, docker.Client) {
125+
t.Helper()
126+
127+
// Create temporary home directory
128+
home := t.TempDir()
129+
t.Setenv("HOME", home)
130+
131+
// Create mock Docker client
132+
dockerClient := &fakeDocker{}
133+
134+
return context.Background(), home, dockerClient
135+
}
136+
137+
func writeCatalogFile(t *testing.T, home, content string) {
138+
t.Helper()
139+
140+
// Write catalog file in the catalogs subdirectory
141+
catalogsDir := filepath.Join(home, ".docker", "mcp", "catalogs")
142+
err := os.MkdirAll(catalogsDir, 0o755)
143+
require.NoError(t, err)
144+
145+
catalogFile := filepath.Join(catalogsDir, catalog.DockerCatalogFilename)
146+
err = os.WriteFile(catalogFile, []byte(content), 0o644)
147+
require.NoError(t, err)
148+
149+
// Create catalog.json registry file to register the docker-mcp catalog
150+
catalogRegistry := `{
151+
"catalogs": {
152+
"docker-mcp": {
153+
"displayName": "Docker MCP Default Catalog",
154+
"url": "docker-mcp.yaml",
155+
"lastUpdate": "2024-01-01T00:00:00Z"
156+
}
157+
}
158+
}`
159+
mcpDir := filepath.Join(home, ".docker", "mcp")
160+
catalogRegistryFile := filepath.Join(mcpDir, "catalog.json")
161+
err = os.WriteFile(catalogRegistryFile, []byte(catalogRegistry), 0o644)
162+
require.NoError(t, err)
163+
164+
// Create empty tools.yaml to avoid Docker volume access
165+
// This prevents the Inspect function from trying to read tool config from Docker
166+
toolsFile := filepath.Join(mcpDir, "tools.yaml")
167+
err = os.WriteFile(toolsFile, []byte(""), 0o644)
168+
require.NoError(t, err)
169+
}

0 commit comments

Comments
 (0)