Skip to content

Commit b810634

Browse files
authored
Tidy up command structure for MCP Publisher (#324)
## Summary - Improved command structure and organization for the MCP Publisher CLI - Enhanced readability and maintainability of publisher commands - Cleaned up publish and edit handler logic This builds on the previous cleanup work to further streamline the publisher interface. This also works as part of #89
1 parent edf3929 commit b810634

File tree

10 files changed

+883
-884
lines changed

10 files changed

+883
-884
lines changed

tests/integration/main.go

Lines changed: 111 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -20,77 +20,55 @@ import (
2020

2121
const registryURL = "http://localhost:8080"
2222

23-
var publishedIDRegex = regexp.MustCompile(`"x-io\.modelcontextprotocol\.registry":\s*\{[^}]*"id":\s*"([^"]+)"`)
24-
2523
func main() {
2624
log.SetFlags(0)
2725
if err := run(); err != nil {
2826
log.Fatal(err)
2927
}
3028
}
3129

32-
func getAnonymousToken() (string, error) {
33-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
34-
defer cancel()
35-
36-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, registryURL+"/v0/auth/none", nil)
30+
func run() error {
31+
examplesPath := filepath.Join("docs", "server-json", "examples.md")
32+
examples, err := getExamples(examplesPath)
3733
if err != nil {
38-
return "", fmt.Errorf("failed to create request: %w", err)
34+
log.Fatalf("failed to extract examples: %v", err)
3935
}
40-
req.Header.Set("Content-Type", "application/json")
36+
log.Printf("Found %d examples in %q\n", len(examples), examplesPath)
4137

42-
resp, err := http.DefaultClient.Do(req)
38+
// Set up authentication using the new login workflow
39+
err = setupPublisherAuth()
4340
if err != nil {
44-
return "", fmt.Errorf("failed to get anonymous token: %w", err)
45-
}
46-
defer resp.Body.Close()
47-
48-
if resp.StatusCode != http.StatusOK {
49-
body, _ := io.ReadAll(resp.Body)
50-
return "", fmt.Errorf("auth endpoint returned %d: %s", resp.StatusCode, string(body))
51-
}
52-
53-
var tokenResponse struct {
54-
RegistryToken string `json:"registry_token"`
55-
ExpiresAt int `json:"expires_at"`
41+
log.Fatalf("failed to set up publisher auth: %v", err)
5642
}
43+
defer cleanupPublisherAuth()
5744

58-
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
59-
return "", fmt.Errorf("failed to decode token response: %w", err)
60-
}
45+
return publish(examples)
46+
}
6147

62-
if tokenResponse.RegistryToken == "" {
63-
return "", fmt.Errorf("received empty token from auth endpoint")
64-
}
48+
func setupPublisherAuth() error {
49+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
50+
defer cancel()
6551

66-
log.Printf("Got anonymous token (expires at %d)", tokenResponse.ExpiresAt)
67-
return tokenResponse.RegistryToken, nil
68-
}
52+
cmd := exec.CommandContext(ctx, "./bin/publisher", "login", "none", "--registry", registryURL)
53+
cmd.WaitDelay = 100 * time.Millisecond
6954

70-
func run() error {
71-
examplesPath := filepath.Join("docs", "server-json", "examples.md")
72-
examples, err := examples(examplesPath)
55+
out, err := cmd.CombinedOutput()
7356
if err != nil {
74-
if errors.Is(err, os.ErrNotExist) {
75-
log.Fatalf("%q not found; run this test from the repo root", examplesPath)
76-
}
77-
log.Fatalf("failed to extract examples from %q: %v", examplesPath, err)
57+
return fmt.Errorf("login failed: %s", string(out))
7858
}
7959

80-
log.Printf("Found %d examples in %q\n", len(examples), examplesPath)
60+
log.Printf("Publisher login successful: %s", strings.TrimSpace(string(out)))
61+
return nil
62+
}
8163

82-
// Get anonymous token from the none endpoint
83-
token, err := getAnonymousToken()
64+
func cleanupPublisherAuth() {
65+
// Clean up the token file
66+
homeDir, err := os.UserHomeDir()
8467
if err != nil {
85-
log.Fatalf("failed to get anonymous token: %v", err)
68+
return
8669
}
87-
88-
if err := os.WriteFile(".mcpregistry_registry_token", []byte(token), 0600); err != nil {
89-
log.Fatalf("failed to write token: %v", err)
90-
}
91-
defer os.Remove(".mcpregistry_registry_token")
92-
93-
return publish(examples)
70+
tokenPath := filepath.Join(homeDir, ".mcp_publisher_token")
71+
os.Remove(tokenPath)
9472
}
9573

9674
func publish(examples []example) error {
@@ -177,24 +155,95 @@ func publishToRegistry(expected map[string]any, line int) error {
177155
func runPublisher(filePath string) (string, error) {
178156
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
179157
defer cancel()
180-
cmd := exec.CommandContext(ctx, "./bin/publisher", "publish", "--mcp-file", filePath, "--registry-url", registryURL, "--auth-method", "none")
158+
159+
cmd := exec.CommandContext(ctx, "./bin/publisher", "publish", filePath)
181160
cmd.WaitDelay = 100 * time.Millisecond
182161

183162
out, err := cmd.CombinedOutput()
184-
if errors.Is(err, exec.ErrNotFound) || errors.Is(err, os.ErrNotExist) {
185-
return "", errors.New("publisher not found; did you run tests/integration/run.sh?")
186-
}
187163
output := strings.TrimSpace("publisher output:\n\t" + strings.ReplaceAll(string(out), "\n", "\n \t"))
188164
if err != nil {
189165
return "", errors.New(output)
190166
}
191167
log.Println(" ✅", output)
192168

193-
m := publishedIDRegex.FindStringSubmatch(output)
194-
if len(m) != 2 || m[1] == "" {
195-
return "", errors.New("didn't find ID in publisher output")
169+
// Get the server name from the file to look up the ID
170+
serverName, err := getServerNameFromFile(filePath)
171+
if err != nil {
172+
return "", fmt.Errorf("failed to get server name from file: %w", err)
173+
}
174+
175+
// Find the server in the registry by name
176+
return findServerIDByName(serverName)
177+
}
178+
179+
func getServerNameFromFile(filePath string) (string, error) {
180+
data, err := os.ReadFile(filePath)
181+
if err != nil {
182+
return "", err
183+
}
184+
185+
var serverData map[string]any
186+
if err := json.Unmarshal(data, &serverData); err != nil {
187+
return "", err
188+
}
189+
190+
// Handle both old ServerDetail format and new PublishRequest format
191+
if server, exists := serverData["server"]; exists {
192+
// New PublishRequest format
193+
if serverMap, ok := server.(map[string]any); ok {
194+
if name, ok := serverMap["name"].(string); ok {
195+
return name, nil
196+
}
197+
}
198+
} else if name, ok := serverData["name"].(string); ok {
199+
// Old ServerDetail format
200+
return name, nil
201+
}
202+
203+
return "", errors.New("could not find server name in file")
204+
}
205+
206+
func findServerIDByName(serverName string) (string, error) {
207+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
208+
defer cancel()
209+
210+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, registryURL+"/v0/servers", nil)
211+
if err != nil {
212+
return "", err
213+
}
214+
215+
resp, err := http.DefaultClient.Do(req)
216+
if err != nil {
217+
return "", err
218+
}
219+
defer resp.Body.Close()
220+
221+
if resp.StatusCode != http.StatusOK {
222+
body, _ := io.ReadAll(resp.Body)
223+
return "", fmt.Errorf("registry responded %d: %s", resp.StatusCode, string(body))
224+
}
225+
226+
var serverList struct {
227+
Servers []map[string]any `json:"servers"`
196228
}
197-
return m[1], nil
229+
if err := json.NewDecoder(resp.Body).Decode(&serverList); err != nil {
230+
return "", fmt.Errorf("failed to decode server list: %w", err)
231+
}
232+
233+
// Find the server with matching name
234+
for _, server := range serverList.Servers {
235+
if registryMeta, ok := server["x-io.modelcontextprotocol.registry"].(map[string]any); ok {
236+
if id, ok := registryMeta["id"].(string); ok {
237+
if serverData, ok := server["server"].(map[string]any); ok {
238+
if name, ok := serverData["name"].(string); ok && name == serverName {
239+
return id, nil
240+
}
241+
}
242+
}
243+
}
244+
}
245+
246+
return "", fmt.Errorf("could not find server with name %s", serverName)
198247
}
199248

200249
func verifyPublishedServer(id string, expected map[string]any) error {
@@ -221,20 +270,20 @@ func verifyPublishedServer(id string, expected map[string]any) error {
221270
if err := json.Unmarshal(content, &actual); err != nil {
222271
return fmt.Errorf("failed to unmarshal registry response: %w", err)
223272
}
224-
273+
225274
// Both API response and expected are now in extension wrapper format
226275
// Compare the server portions of both
227276
actualServer, ok := actual["server"]
228277
if !ok {
229278
return fmt.Errorf("expected server field in registry response")
230279
}
231-
280+
232281
// Extract expected server portion for comparison
233282
expectedServer := expected
234283
if server, exists := expected["server"]; exists {
235284
expectedServer = server.(map[string]any)
236285
}
237-
286+
238287
if err := compare(expectedServer, actualServer); err != nil {
239288
return fmt.Errorf(`example "%s": %w`, expectedServer["name"], err)
240289
}
@@ -246,9 +295,10 @@ type example struct {
246295
line int
247296
}
248297

249-
func examples(path string) ([]example, error) {
298+
func getExamples(path string) ([]example, error) {
250299
b, err := os.ReadFile(path)
251300
if err != nil {
301+
log.Fatalf("examples not found; run this test from the repo root")
252302
return nil, err
253303
}
254304

0 commit comments

Comments
 (0)