Skip to content

Conversation

@pree-dew
Copy link
Contributor

Motivation and Context

Deprecation and yanking are standard requirements when it comes to manages servers(packages), many use cases like rebranding, security vulnerabilities in packages etc demand these kind of features.

How Has This Been Tested?

This has been tested via test cases and local setup

Breaking Changes

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@pree-dew
Copy link
Contributor Author

Fixes #637

Copy link
Member

@tadasant tadasant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @pree-dew for taking this on!

Added inline comments, and also: let's update docs/reference/cli/commands.md with notes on how to use the new CLI commands/options.

Some additional feedback by way of Claude (that I've picked out and agree with):

1. newName pointing to itself

There's no check preventing:

mcp-publisher status io.github.user/my-server --all-versions --status deprecated \
  --new-name io.github.user/my-server

The validateNewName function checks that the new server exists and user has permissions, but doesn't check if newName == currentServerName. This would create a self-referential deprecation.

2. --all-versions isn't atomic

Looking at updateAllVersionsStatus (lines 123-157):

for _, v := range versions {
    if err := updateServerStatus(...); err != nil {
        failureCount++
    } else {
        successCount++
    }
}

Each version is updated in a separate HTTP request. If one fails mid-way:

  • Some versions are deprecated, others aren't
  • No rollback mechanism
  • The newName could be set on some versions but not others

Can we make this happen in a transaction?

3. newName without --all-versions at API level

The CLI enforces --new-name requires --all-versions, but the API doesn't:

// In validateNewName - only checks status, not all-versions
if input.Status != string(model.StatusDeprecated) && input.Status != string(model.StatusYanked) {
    return huma.Error400BadRequest("new_name can only be used with deprecated or yanked status")
}

Someone hitting the API directly could set newName on a single version, leaving other versions without it. This creates inconsistent state.

StatusActive Status = "active"
StatusDeprecated Status = "deprecated"
StatusDeleted Status = "deleted"
StatusYanked Status = "yanked"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree with the semantic reason for this change, I worry a bit about this being a breaking change. I think we don't currently have any data in the official registry using deleted, but I suspect a lot of downstream consumers do.

So by default, I'd be inclined to keep it deleted (knowing we really mean yanked), and maybe leave an issue as something we consider addressing for a bigger, breaking GA launch of the registry.

Status model.Status `json:"status" enum:"active,deprecated,yanked" doc:"Server lifecycle status"`
StatusChangedAt time.Time `json:"statusChangedAt" format:"date-time" doc:"Timestamp when the server status was last changed"`
StatusMessage *string `json:"statusMessage,omitempty" doc:"Optional message explaining status change (e.g., deprecation reason, migration guidance)"`
AlternativeURL *string `json:"alternativeUrl,omitempty" format:"uri" doc:"Optional URL to alternative/replacement server for deprecated or yanked servers"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need AlternativeURL in addition to StatusMessage and NewName? It feels redundant to me, and if someone really wants a non-registry reference (which would be NewName), they could put the URL in the StatusMessage. Or is there compelling reason to have the structured AlternativeURL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with a dedicated field, consumers/tools can:

  • Automatically create clickable links in IDEs/terminals
  • Extract URLs programmatically without regex parsing
  • Validate that it's actually a valid URL
  • Different authors format differently, making it hard for tooling to extract URLs reliably.
  • The URL might not always be a direct alternative:
    • Migration guides
    • Documentation explaining the deprecation
    • Security advisories
    • Company announcements
  • Package registries can enforce:
    • URL format validation
    • HTTPS requirement
    • Domain allowlists
    • Link checking

Comment on lines 115 to 117
// For now, only allow status changes for admins
// Future: Implement logic to allow server authors to change active <-> deprecated
// but only admins can set to deleted
// but only admins can set to yanked
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is now outdated; should delete

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixed

Authorization string `header:"Authorization" doc:"Registry JWT token with edit permissions" required:"true"`
ServerName string `path:"serverName" doc:"URL-encoded server name" example:"com.example%2Fmy-server"`
Version string `path:"version" doc:"URL-encoded version to edit" example:"1.0.0"`
Status string `query:"status" doc:"New status for the server (active, deprecated, yanked)" required:"false" enum:"active,deprecated,yanked"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint design feels awkward to me. I know we had precedent here with status already, but I think overloading this shape with being responsible for both Body (core ServerJSON) changes as well as status & associated fields (statusmessage, newname)

Should we move status wrangling over to a dedicated PATCH endpoint?

To-date, status has only been an admin tool anyway, so I'm not worried about a breaking change in the API here.

Copy link
Contributor Author

@pree-dew pree-dew Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Added the details of new endpoints in the comment. #893 (comment)

apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
)

func StatusCommand(args []string) error {
Copy link
Member

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, like mcp-publisher update, mcp-publisher patch or something similar?

Copy link
Contributor Author

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:

  1. Domain clarity: Status describes the domain action
    (lifecycle management), while update/patch describes
    the technical operation. Domain-focused naming is
    better for CLI UX.
  2. Separation of concerns: The project already has clear separation:
  • publish → add new version
  • edit → update server configuration
  • status → manage lifecycle state
  1. Constraint coupling: Since newName is ONLY valid with deprecated/yanked status, it's fundamentally part of the status workflow, not independent metadata.

  2. 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.

@pree-dew
Copy link
Contributor Author

@tadasant Thank you for the review, I will address the points.

Also wanted your feedback on these questions:

Do we need bulk endpoint for status transitions, renaming for all servers?
Do we want to allow publisher to make status transition?
Do we want yanked package discoverable?

@tadasant
Copy link
Member

Do we need bulk endpoint for status transitions, renaming for all servers?

If I understand the question correctly, we'd want the ability to e.g. "deprecate all versions of server with name x" for the case where we want to deprecate --all-versions; yes. We wouldn't need something like "deprecate all versions of all servers with names x, y, and z all together in one API call".

Do we want to allow publisher to make status transition?

Yes.

Do we want yanked package discoverable?

Ideally: it should show up on /servers?updated_since calls, but not by default on server details / server version history calls unless we provide some explicit query param that "unhides" them.

@pree-dew
Copy link
Contributor Author

If I understand the question correctly, we'd want the ability to e.g. "deprecate all versions of server with name x" for the case where we want to deprecate --all-versions; yes.

Yes, for all versions of a server. Noted.

I will accommodate recommended changes.

@pree-dew
Copy link
Contributor Author

New Endpoints

Update Single Version Status

Endpoint: PATCH /v0/servers/{serverName}/versions/{version}/status

Summary: Update MCP server status

Description: Update the status metadata of a specific version of an MCP server.
Requires edit permission for the server.
Request Body:

{
"status": "deprecated",
"statusMessage": "This server is deprecated, please migrate to v2",
"alternativeUrl": "https://github.com/example/new-server",
"newName": "com.example/new-server-name"
}

Update All Versions Status

Endpoint: PATCH /v0/servers/{serverName}/status

Summary: Update status for all versions of an MCP server

Description: Update the status metadata of all versions in a single transaction.
Either all versions are updated or none on failure.
Request Body:

{
"status": "deprecated",
"statusMessage": "This server is deprecated, please migrate to v2",
"alternativeUrl": "https://github.com/example/new-server",
"newName": "com.example/new-server-name"
}

@pree-dew
Copy link
Contributor Author

Do we need bulk endpoint for status transitions, renaming for all servers?

Added this endpoint

Do we want yanked package discoverable?

Added a query parameter include_yanked=true in ListAllServers for unhiding yanked server or versions. For specific version queries and with updated_since it will return all versions.

@pree-dew
Copy link
Contributor Author

pree-dew commented Jan 21, 2026

All review comments are addressed including Claude ones also.

Few questions @tadasant @rdimitrov

  1. Do we have to allow to publish permissions users to make status transitions? (Currently it is edit)
  2. Do we want to change yanked to delete? It doesn't align with industry standard.
  3. Earlier edit endpoint was under admin tag, any specific reason for this?

Let me know if the changes makes sense, I will update the documentation accordingly. Happy to hear your feedback on PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants