-
Notifications
You must be signed in to change notification settings - Fork 567
Deprecated, yanked and partial renaming 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
Open
pree-dew
wants to merge
13
commits into
modelcontextprotocol:main
Choose a base branch
from
pree-dew:deprecated-with-yanked
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
17b1715
deprecated-with-yanked: first take with possibility of renaming
pree-dew 4ad7275
some minor fixes for comment and instead of checking edit permission …
pree-dew fcb4e19
fix linting and formating issues
pree-dew c0e9eb2
fix linting and formating issues
pree-dew 742660b
fix linting and formating issues
pree-dew 9cbb049
fix merge conflicts
pree-dew 92020ef
remove unused import
pree-dew c1a025b
fix gofmt issues
pree-dew d81d440
add bulk endpoint for all versions of server, update cli, include fil…
pree-dew c5bf5c8
fix lint issues
pree-dew 702ad43
apply de morgan's law
pree-dew 491c664
make validation logic simpler
pree-dew 34c0cf8
reduce the cyclomatic complexity
pree-dew File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about calling this command
status. It's also used for setting e.g.newName, which isn't necessarily associated with a status change. Can we make it more generic, likemcp-publisher update,mcp-publisher patchor something similar?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am happy to accommodate this change. Reasons that I thought are:
(lifecycle management), while update/patch describes
the technical operation. Domain-focused naming is
better for CLI UX.
Constraint coupling: Since newName is ONLY valid with deprecated/yanked status, it's fundamentally part of the status workflow, not independent metadata.
User mental model: When users want to deprecate/yank a server, they'll naturally think "status". When they want to change description/remotes, they'll think "edit".
Happy to hear your thoughts on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair points -- I think I'm definitely OK with keeping it
statusif we end up agreeing to dropnewNamefrom this work.