-
Notifications
You must be signed in to change notification settings - Fork 790
Publisher-managed "deleted" support #893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
17b1715
4ad7275
fcb4e19
c0e9eb2
742660b
9cbb049
92020ef
c1a025b
d81d440
c5bf5c8
702ad43
491c664
34c0cf8
9869bc6
8ae0ddf
8852ccd
7d09b36
f0ee46c
9e4566f
50590fb
7a42fd4
7884574
1ee1521
dc45fbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| package commands | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "flag" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| ) | ||
|
|
||
| // StatusUpdateRequest represents the request body for status update endpoints | ||
| type StatusUpdateRequest struct { | ||
| Status string `json:"status"` | ||
| StatusMessage *string `json:"statusMessage,omitempty"` | ||
| AlternativeURL *string `json:"alternativeUrl,omitempty"` | ||
| NewName *string `json:"newName,omitempty"` | ||
| } | ||
|
|
||
| // AllVersionsStatusResponse represents the response from the all-versions status endpoint | ||
| type AllVersionsStatusResponse struct { | ||
| UpdatedCount int `json:"updatedCount"` | ||
| } | ||
|
|
||
| func StatusCommand(args []string) error { | ||
| // Parse command flags | ||
| fs := flag.NewFlagSet("status", flag.ExitOnError) | ||
| status := fs.String("status", "", "New status: active, deprecated, or yanked (required)") | ||
| message := fs.String("message", "", "Optional status message explaining the change") | ||
| alternativeURL := fs.String("alternative-url", "", "Optional URL to alternative/replacement server") | ||
| newName := fs.String("new-name", "", "Optional new server name when server has been renamed") | ||
| allVersions := fs.Bool("all-versions", false, "Apply status change to all versions of the server") | ||
|
|
||
| if err := fs.Parse(args); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Validate required arguments | ||
| if *status == "" { | ||
| return errors.New("--status flag is required (active, deprecated, or yanked)") | ||
| } | ||
|
|
||
| // Validate status value | ||
| validStatuses := map[string]bool{"active": true, "deprecated": true, "yanked": true} | ||
| if !validStatuses[*status] { | ||
| return fmt.Errorf("invalid status '%s'. Must be one of: active, deprecated, yanked", *status) | ||
| } | ||
|
|
||
| // Get server name from positional args | ||
| remainingArgs := fs.Args() | ||
| if len(remainingArgs) < 1 { | ||
| return errors.New("server name is required\n\nUsage: mcp-publisher status <server-name> [version] --status <active|deprecated|yanked> [flags]") | ||
| } | ||
|
|
||
| serverName := remainingArgs[0] | ||
| var version string | ||
|
|
||
| // Get version if provided (required unless --all-versions is set) | ||
| if !*allVersions { | ||
| if len(remainingArgs) < 2 { | ||
| return errors.New("version is required unless --all-versions flag is set\n\nUsage: mcp-publisher status <server-name> <version> --status <active|deprecated|yanked> [flags]") | ||
| } | ||
| version = remainingArgs[1] | ||
| } | ||
|
|
||
| // Validate new-name parameter constraints | ||
| if *newName != "" { | ||
| // Validation: new-name requires deprecated or yanked status | ||
| if *status != "deprecated" && *status != "yanked" { | ||
| return errors.New("--new-name can only be used with --status deprecated or --status yanked") | ||
| } | ||
| // Validation: new-name requires --all-versions flag | ||
| if !*allVersions { | ||
| return errors.New("--new-name requires --all-versions flag") | ||
| } | ||
| } | ||
|
|
||
| // Load saved token | ||
| homeDir, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get home directory: %w", err) | ||
| } | ||
|
|
||
| tokenPath := filepath.Join(homeDir, TokenFileName) | ||
| tokenData, err := os.ReadFile(tokenPath) | ||
| if err != nil { | ||
| if os.IsNotExist(err) { | ||
| return errors.New("not authenticated. Run 'mcp-publisher login <method>' first") | ||
| } | ||
| return fmt.Errorf("failed to read token: %w", err) | ||
| } | ||
|
|
||
| var tokenInfo map[string]string | ||
| if err := json.Unmarshal(tokenData, &tokenInfo); err != nil { | ||
| return fmt.Errorf("invalid token data: %w", err) | ||
| } | ||
|
|
||
| token := tokenInfo["token"] | ||
| registryURL := tokenInfo["registry"] | ||
| if registryURL == "" { | ||
| registryURL = DefaultRegistryURL | ||
| } | ||
|
|
||
| // Update status | ||
| if *allVersions { | ||
| return updateAllVersionsStatus(registryURL, serverName, *status, *message, *alternativeURL, *newName, token) | ||
| } | ||
| return updateVersionStatus(registryURL, serverName, version, *status, *message, *alternativeURL, *newName, token) | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking; let's probably take this as a fast-follow (or just file an issue if you don't think you'll be able to get to it soon) -- Two UX concerns with the CLI: 1. No confirmation on Running I don't think we need confirmation for single-version updates; those are targeted enough. 2. No "from → to" in the output Currently the output only says what we're changing to: It should show the previous status so the user can easily undo a mistake: We should do this for both the single-version changes and the bulk changes. |
||
|
|
||
| func updateVersionStatus(registryURL, serverName, version, status, statusMessage, alternativeURL, newName, token string) error { | ||
| _, _ = fmt.Fprintf(os.Stdout, "Updating %s version %s to status: %s\n", serverName, version, status) | ||
|
|
||
| if err := updateServerStatus(registryURL, serverName, version, status, statusMessage, alternativeURL, newName, token); err != nil { | ||
| return fmt.Errorf("failed to update status: %w", err) | ||
| } | ||
|
|
||
| _, _ = fmt.Fprintln(os.Stdout, "✓ Successfully updated status") | ||
| return nil | ||
| } | ||
|
|
||
| func updateAllVersionsStatus(registryURL, serverName, status, statusMessage, alternativeURL, newName, token string) error { | ||
| _, _ = fmt.Fprintf(os.Stdout, "Updating all versions of %s to status: %s\n", serverName, status) | ||
|
|
||
| if !strings.HasSuffix(registryURL, "/") { | ||
| registryURL += "/" | ||
| } | ||
|
|
||
| // Build the request body | ||
| requestBody := StatusUpdateRequest{ | ||
| Status: status, | ||
| } | ||
| if statusMessage != "" { | ||
| requestBody.StatusMessage = &statusMessage | ||
| } | ||
| if alternativeURL != "" { | ||
| requestBody.AlternativeURL = &alternativeURL | ||
| } | ||
| if newName != "" { | ||
| requestBody.NewName = &newName | ||
| } | ||
|
|
||
| jsonData, err := json.Marshal(requestBody) | ||
| if err != nil { | ||
| return fmt.Errorf("error serializing request: %w", err) | ||
| } | ||
|
|
||
| // URL encode the server name | ||
| encodedServerName := url.PathEscape(serverName) | ||
| statusURL := registryURL + "v0/servers/" + encodedServerName + "/status" | ||
|
|
||
| req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, statusURL, bytes.NewBuffer(jsonData)) | ||
| if err != nil { | ||
| return fmt.Errorf("error creating request: %w", err) | ||
| } | ||
| req.Header.Set("Content-Type", "application/json") | ||
| req.Header.Set("Authorization", "Bearer "+token) | ||
|
|
||
| client := &http.Client{} | ||
| resp, err := client.Do(req) | ||
| if err != nil { | ||
| return fmt.Errorf("error sending request: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return fmt.Errorf("error reading response: %w", err) | ||
| } | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return fmt.Errorf("server returned status %d: %s", resp.StatusCode, body) | ||
| } | ||
|
|
||
| // Parse response to get updated count | ||
| var response AllVersionsStatusResponse | ||
| if err := json.Unmarshal(body, &response); err != nil { | ||
| // If we can't parse the response, just report success | ||
| _, _ = fmt.Fprintln(os.Stdout, "✓ Successfully updated all versions") | ||
| return nil | ||
| } | ||
|
|
||
| _, _ = fmt.Fprintf(os.Stdout, "✓ Successfully updated %d version(s)\n", response.UpdatedCount) | ||
| return nil | ||
| } | ||
|
|
||
| func updateServerStatus(registryURL, serverName, version, status, statusMessage, alternativeURL, newName, token string) error { | ||
| if !strings.HasSuffix(registryURL, "/") { | ||
| registryURL += "/" | ||
| } | ||
|
|
||
| // Build the request body | ||
| requestBody := StatusUpdateRequest{ | ||
| Status: status, | ||
| } | ||
| if statusMessage != "" { | ||
| requestBody.StatusMessage = &statusMessage | ||
| } | ||
| if alternativeURL != "" { | ||
| requestBody.AlternativeURL = &alternativeURL | ||
| } | ||
| if newName != "" { | ||
| requestBody.NewName = &newName | ||
| } | ||
|
|
||
| jsonData, err := json.Marshal(requestBody) | ||
| if err != nil { | ||
| return fmt.Errorf("error serializing request: %w", err) | ||
| } | ||
|
|
||
| // URL encode the server name and version | ||
| encodedServerName := url.PathEscape(serverName) | ||
| encodedVersion := url.PathEscape(version) | ||
| statusURL := registryURL + "v0/servers/" + encodedServerName + "/versions/" + encodedVersion + "/status" | ||
|
|
||
| req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, statusURL, bytes.NewBuffer(jsonData)) | ||
| if err != nil { | ||
| return fmt.Errorf("error creating request: %w", err) | ||
| } | ||
| req.Header.Set("Content-Type", "application/json") | ||
| req.Header.Set("Authorization", "Bearer "+token) | ||
|
|
||
| client := &http.Client{} | ||
| resp, err := client.Do(req) | ||
| if err != nil { | ||
| return fmt.Errorf("error sending request: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return fmt.Errorf("error reading response: %w", err) | ||
| } | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return fmt.Errorf("server returned status %d: %s", resp.StatusCode, body) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.