Skip to content

feat: add cargo (crates.io) as a package registry type#1207

Open
Wolfe-Jam wants to merge 3 commits into
modelcontextprotocol:mainfrom
Wolfe-Jam:feat/cargo-package-registry-support
Open

feat: add cargo (crates.io) as a package registry type#1207
Wolfe-Jam wants to merge 3 commits into
modelcontextprotocol:mainfrom
Wolfe-Jam:feat/cargo-package-registry-support

Conversation

@Wolfe-Jam
Copy link
Copy Markdown

@Wolfe-Jam Wolfe-Jam commented Apr 26, 2026

Update — 2026-06-01 (commit 1f3be53)

  • All @P4ST4S review feedback addressed — positive-path hermetic mock + live anchor against rust-faf-mcp v0.3.1; 5xx routed as transient; doc comment qualified. See the close-out comment for the per-item breakdown.
  • Runtime-model question RESOLVEDpackage-types.mdx documents cargo and MCPB as both first-class Rust MCP distribution paths (cargo for source-distributed, MCPB for prebuilt-binary). The "Open question" section below was the original draft framing; the doc now ships the resolution.
  • Cargo-specific gotcha discovered and documented — crates.io strips HTML comments during README rendering, unlike PyPI/NuGet. package-types.mdx now spells this out so cargo authors don't ship invisible mcp-name tokens (which is exactly what happened to rust-faf-mcp v0.3.0; fixed in v0.3.1).
  • Stale reference removed in this update — the "Out of scope" line mentioning docs/guides/publishing/publish-cargo.md pointed at a path that doesn't exist. The canonical per-type doc is docs/modelcontextprotocol-io/package-types.mdx; the Cargo section has been added there directly.
  • Two real-world Rust MCP servers queued for day-one register: rust-faf-mcp v0.3.1 (live, end-to-end validated by the new live test) and perfetto-mcp-rs (@0xZOne in Support crates.io as a package registry type #1055).
  • Tests: 19/19 cargo tests pass (16 existing + 2 hermetic + 1 live anchor). make validate clean. golangci-lint 0 issues.

Adds support for registryType: cargo so Rust MCP servers published to crates.io can be registered through the
documented validation flow rather than the MCPB binary-packaging workaround. Closes #1055.

Motivation and Context

#1055 noted that crates.io has ~1,800 MCP-related packages with no direct path into the registry — only the MCPB
binary-packaging workaround. This PR adds first-class support for cargo as a package registry type: schema +
API surface, validator, integration tests, and a documented example. The publishing guide
(docs/guides/publishing/publish-cargo.md) follows on this branch once the runtime-model direction in
Additional context is settled.

How Has This Been Tested?

  • make validate — clean. Schema-vs-openapi sync verified; 17/17 examples in generic-server-json.md
    validate against the schema; expectedServerJSONCount bumped 16 → 17 to match the new Cargo example.
  • go test ./internal/validators/... — passes. 16 cargo validator sub-cases across 4 test functions, run
    against real crates.io (~2.5s wall):
    • Input validation rejection paths
    • Registry-baseURL rejection (4 variants: different host, trailing slash, http-not-https, subdomain typo)
    • Ownership validation against real crates (serde, tokio, rand) — all correctly rejected for missing
      mcp-name token
    • Server-name format variations (canonical io.github.OWNER/REPO, multi-hyphen, underscore, numeric suffix)
  • Positive-path test — gated on rust-faf-mcp v0.2.3+ being published with mcp-name: io.github.Wolfe-Jam/rust-faf-mcp in its README. Reserved as a TODO in cargo_test.go; uncomments to become the
    live anchor once that publish lands.
  • Local make test-unit — not run due to a Go toolchain version quirk on my workstation (project
    auto-downloads 1.26.0; my local go is 1.25.6, causing compile mismatch). CI will exercise the full
    PostgreSQL-backed suite.

Breaking Changes

None. This is an additive change — cargo joins the supported registryType enum alongside
npm/pypi/nuget/oci/mcpb. Existing publishes continue to work unchanged.

Types of changes

  • New feature (non-breaking change which adds functionality)
  • Documentation update (new Cargo example in generic-server-json.md)

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

Validator design — two-call retrieval on the documented API

internal/validators/registries/cargo.go mirrors the PyPI validator's README-token approach (substring-match
mcp-name: <serverName>), with a two-call retrieval pattern to stay on the documented public crates.io API:

  1. GET /api/v1/crates/{name}/{version}/readme returns 200 OK with a JSON pointer ({"url": "https://static.crates.io/readmes/.../...html"}) — crates.io hands us the URL rather than emitting a 302.
  2. Follow the pointer to the rendered HTML; substring-match for the token.

The two-call pattern stays on the documented public surface. The CDN URL layout is observed-stable, but treating
it as the entry point would mean depending on an undocumented path. With two calls, crates.io controls where
the README lives — if they move it, the metadata endpoint hands us the new URL.

Missing crates and missing versions surface as 403 from the CDN (S3's default for missing keys), not 404.
The validator treats any non-200 as "not found" and surfaces the actual status code in the error message.

Open question — runtime model

Cargo's runtime model is genuinely different from npm/PyPI/NuGet. cargo install is one-time (binary lands on
PATH at ~/.cargo/bin), not per-invocation like npx, uvx, or dnx (the new one in .NET 10 SDK Preview 6+).
The new Cargo example in generic-server-json.md annotates this honestly and omits runtimeHint.

If a different framing is preferred — e.g. recommending MCPB (prebuilt binary distribution via GitHub
Releases) as the primary path for Rust MCP servers instead of cargo — happy to adjust the documentation
accordingly. The schema + validator code in this PR is additive and doesn't force a recommendation either
way; a Rust author who chooses cargo can use it, and one who prefers MCPB still can.

Out of scope (deferred)

  • docs/guides/publishing/publish-cargo.md — follows on this branch once the runtime-model direction above
    is settled.
  • Publisher CLI Cargo.toml autodetect in cmd/publisher/commands/init.go — separate concern, separate PR.
  • Positive-path validator test — gated on rust-faf-mcp v0.2.3+ publishing as noted in How Has This Been
    Tested?
    .

modelcontextprotocol#1055 noted that crates.io has ~1,800 MCP-related packages with no
direct path into the registry, only the MCPB binary-packaging
workaround. This commit adds the schema-side wiring for
`registryType: cargo`:

- `pkg/model/constants.go`: `RegistryTypeCargo` + `RegistryURLCrates`
- `server.json` schema and `openapi.yaml`: `cargo` in the example
  enum for `registryType`; `https://crates.io` in `registryBaseUrl`
- `generic-server-json.md`: new minimal Cargo example, with a
  runtime-model note. `cargo install` puts the binary on PATH at
  `~/.cargo/bin` and the MCP client invokes it by name. `npx`
  (npm), `uvx` (PyPI), and `dnx` (NuGet, .NET 10 SDK) were the
  cross-ecosystem precedents considered; cargo has no single-shot
  analog, so `runtimeHint` is omitted.
- `tools/validate-examples/main.go`: `expectedServerJSONCount`
  bumped 16 → 17 to match the new example (caught by `make validate`).

Validator and `publish-cargo.md` follow on this branch once the
schema-side direction is settled.

Refs modelcontextprotocol#1055
Closes modelcontextprotocol#1055.

Verification mirrors the PyPI validator: substring-match
`mcp-name: <serverName>` against the package's rendered README.
The publisher adds a single line to their README before publishing.

Two-call retrieval pattern:

1. `GET /api/v1/crates/{name}/{version}/readme` returns 200 with a
   JSON pointer `{"url": "https://static.crates.io/readmes/.../...html"}`
   — crates.io hands us the URL rather than emitting a 302.
2. Follow the pointer to the rendered HTML.

The two-call pattern stays on the documented public crates.io API
surface. The CDN URL layout is observed-stable, but treating it as
the entry point would mean depending on an undocumented path. With
two calls, crates.io controls where the README lives.

Missing crates and missing versions surface as 403 from the CDN
(S3's default for missing keys), not 404. The validator treats any
non-200 as "not found" and surfaces the actual status code in the
error message.

Tests are integration-only (matching the npm/pypi pattern). 16
sub-cases across input validation, registry-baseURL rejection
(four variants), ownership against real crates (serde, tokio,
rand), and server-name format variations.

The positive-path case is gated on `rust-faf-mcp` v0.2.3+ being
published with `mcp-name: io.github.Wolfe-Jam/rust-faf-mcp` in
its README — the commented-out test in `cargo_test.go` will
uncomment to become the live anchor once that publish happens.

Refs modelcontextprotocol#1055
@Wolfe-Jam Wolfe-Jam force-pushed the feat/cargo-package-registry-support branch from 26eb180 to 6b2b006 Compare April 26, 2026 04:15
@Wolfe-Jam
Copy link
Copy Markdown
Author

@rdimitrov — hey, any thoughts on the review + the runtime-model direction? Once that's settled I can land publish-cargo.md and close out #1055. Review-ready whenever you have a window — lmk if I can help.

@P4ST4S
Copy link
Copy Markdown

P4ST4S commented May 30, 2026

Hi @Wolfe-Jam, took a careful read through this and ran the two-call flow against live crates.io to verify the API shape. The PR follows the PyPI validator pattern cleanly and the technical choices are sound. Two points worth addressing before merge, one of them I'd consider mildly blocking:

Mildly blocking: no positive-path test coverage

The PR description and the comment in cargo_test.go both acknowledge this:

"Positive-path test — gated on rust-faf-mcp v0.2.3+ being published with mcp-name: ... in its README. Reserved as a TODO."

The current 16 test cases all exercise rejection paths. None of them reach the final return nil branch, so a future change like return errors.New(...) (or worse, an accidental return errors.New("..." + serverName) that still happens to satisfy assert.Error) would slip through.

This is solvable today without waiting for a live rust-faf-mcp republish, httptest.Server can mock both the /api/v1/crates/.../readme JSON response and the static.crates.io rendered HTML. Roughly:

mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if strings.HasSuffix(r.URL.Path, "/readme") {
        json.NewEncoder(w).Encode(map[string]string{"url": mock.URL + "/static-readme"})
        return
    }
    fmt.Fprintf(w, "<p>mcp-name: %s</p>", "io.github.test/server")
}))

Then override RegistryBaseURL to mock.URL for the positive case. (You'll need to relax the exact-match RegistryURLCrates check, or expose a test-only constructor, your call on which feels cleaner.)

Worth noting: 5xx surfaces as "not found"

The check if readmeResp.StatusCode != http.StatusOK treats every non-200 as missing. The comment explains the 403-from-S3 case correctly, but it also means a transient 502/503 from the static CDN becomes "cargo package 'foo' version 'X' not found on crates.io", a misleading error for what is actually an upstream availability issue.

PyPI's validator has the same shape (if resp.StatusCode != http.StatusOK), so this isn't a regression in this PR. But if you want to be more diagnostic, splitting 403 → not found from 5xx → transient would be a small win. Defer-able.

Minor: 2-call doc comment is precise but slightly incomplete

The CargoReadmeMetaResponse comment says the endpoint returns 200+JSON "rather than emitting a 302 redirect", true for GET ... Accept: application/json (what the validator does), but GET without the Accept header or HEAD returns 302. The current code is fine because it sets Accept: application/json explicitly, but the comment as written could mislead someone debugging later. A one-line precision like "with Accept: application/json" would close it.


The design choices I particularly liked:

  • url.PathEscape on identifier and version, closes the path-smuggling vector that an unescaped / or .. in identifier would open.
  • User-Agent with contact URL, crates.io's crawler policy explicitly asks for this, glad to see it.
  • Comment explaining 403 from S3 default semantics, non-obvious and exactly the kind of context that saves future debugging.
  • Test against serde/tokio/rand for "no mcp-name" ownership failures, using real high-trust crates that will definitely never claim an MCP namespace is good defense against test flakiness from a stranger's crate adding a mcp-name: line to their README later.

Nice work.

Wolfe-Jam added a commit to Wolfe-Jam/rust-faf-mcp that referenced this pull request Jun 1, 2026
Adds <!-- mcp-name: io.github.Wolfe-Jam/rust-faf-mcp --> to README
as the ownership-verification marker for the upcoming MCP Registry
cargo validator (modelcontextprotocol/registry#1207).

Hidden HTML comment per the documented Cargo ownership-verification
convention; pairs with the existing faf-meta line as substrate metadata.
Validator does a substring match against the rendered README on
crates.io.
Wolfe-Jam added a commit to Wolfe-Jam/rust-faf-mcp that referenced this pull request Jun 1, 2026
v0.3.0 shipped the mcp-name token as an HTML comment, which crates.io
strips during markdown→HTML rendering — the token was invisible to
substring-matching validators (modelcontextprotocol/registry#1207).

Surfaces mcp-name as visible markdown in the README Links section.
v0.3.0 binary is identical and stays installable; v0.3.1 is the
MCP-Registry-verifiable version.

Also:
- Bumps version across Cargo.toml + manifest.json + server.json +
  project.faf + CLAUDE.md (tier4_aero drift-detection enforced).
- Refreshes CLAUDE.md bi-sync timestamp (was 12 weeks stale).
- server.json fileSha256 zero-sentinel for v0.3.1 MCPB package; CHANGELOG
  Known-limitation section explains the chicken-and-egg with CI binary
  build. Intended registration path is registryType: cargo once #1207
  merges.
- Cargo packaging excludes .well-known/ (SEP-2127 staging).
Addresses @P4ST4S's PR modelcontextprotocol#1207 review (all 3 items):

- Positive-path test coverage (mildly blocking) — added two complementary
  tests in cargo_test.go: TestValidateCargo_PositivePathMock (hermetic,
  httptest.Server stand-in for crates.io) and TestValidateCargo_LivePositivePath
  (live anchor against rust-faf-mcp v0.3.1 on real crates.io). The HTTP
  pipeline is split into a package-private validateCargoREADME, exposed
  to tests via export_test.go without weakening the exact-baseURL guard
  in the public ValidateCargo.
- 5xx as transient (defer-able, done anyway) — 403 from static.crates.io
  (S3 default for missing keys) stays 'not found'; 5xx now reports as
  'likely transient, retry later' with the actual status code. Applied
  symmetrically to both the metadata endpoint and the README endpoint.
- CargoReadmeMetaResponse doc comment (minor) — clarified that the
  200+JSON shape requires Accept: application/json; without it, the
  endpoint emits a 302.

Also lands docs/modelcontextprotocol-io/package-types.mdx 'Cargo (Rust)
Packages' section, including:

- Runtime-model resolution: cargo (source-distributed) and MCPB
  (prebuilt-binary) both documented as first-class paths for Rust MCP
  authors; the schema is additive and doesn't force a recommendation.
- Cargo-specific 'gotcha' caveat in the Ownership Verification section:
  crates.io strips HTML comments during markdown→HTML rendering (unlike
  PyPI/NuGet which preserve them), so cargo authors MUST include the
  mcp-name token as visible markdown text — the <!-- ... --> hidden form
  documented elsewhere does not work for cargo. Caught live by shipping
  rust-faf-mcp v0.3.0 with the hidden form (validator rejected), then
  v0.3.1 with the visible form (validator accepts — see live test).

Tests: 19/19 cargo tests pass (16 existing + 2 hermetic + 1 live anchor).
make validate: clean. golangci-lint: 0 issues.

Closes modelcontextprotocol#1055 (once merged + the two queued real-world Rust MCP servers
register: rust-faf-mcp v0.3.1 and perfetto-mcp-rs).
@Wolfe-Jam
Copy link
Copy Markdown
Author

@P4ST4S — thanks for the careful read and the live API verification. All three items addressed in 1f3be53:

Mildly blocking — positive-path test coverage
Added two complementary tests in cargo_test.go:

  • TestValidateCargo_PositivePathMock — hermetic, uses httptest.Server per your snippet. The HTTP pipeline is split into a package-private validateCargoREADME, exposed to tests via a new export_test.go — keeps the exact-baseURL guard intact on the public ValidateCargo while letting tests drive the README-fetch pipeline against a mock.
  • TestValidateCargo_LivePositivePath — live anchor against rust-faf-mcp v0.3.1 on real crates.io (just-published, see notes below). The mock proves the validator works in principle; the live anchor proves it works against the real crates.io API + the static CDN pipeline.

Defer-able — 5xx surfaces as "not found"
Split as you suggested: 403 (S3 default for missing keys) stays as "not found"; 5xx now reports "... — likely transient, retry later" with the actual status code. Applied symmetrically to both the metadata endpoint and the README endpoint. Matches the diagnostic improvement you flagged.

Minor — doc comment precision
CargoReadmeMetaResponse comment now qualifies the 200+JSON shape with Accept: application/json; without that header (or via HEAD), the endpoint emits a 302 instead. The validator already sets Accept: application/json, so behavior is unchanged — just the comment now matches reality for future debuggers.


Cargo-specific gotcha discovered while testing live (and addressed in package-types.mdx):

crates.io strips HTML comments during markdown→HTML README rendering — unlike PyPI/NuGet, which preserve them in their raw description payloads. So the <!-- mcp-name: ... --> hidden-comment form documented for PyPI/NuGet does not work for cargo. The token must be visible markdown text on crates.io.

Caught this by shipping rust-faf-mcp v0.3.0 with the hidden form (validator rejected — token nowhere in the rendered HTML), then v0.3.1 with the token as a visible bullet in the README Links section (validator accepts — see the new live test). The end-to-end smoke is in this PR.

package-types.mdx now spells this out as a "Cargo-specific gotcha" note in the Cargo Ownership Verification section, so future Rust MCP authors don't hit the same wall.


@rdimitrov — quick state-of-the-PR summary:

  • All P4ST4S review addressed (above).
  • Runtime-model question RESOLVED — cargo + MCPB both documented as first-class paths in package-types.mdx (cargo for source-distributed authors, MCPB for prebuilt-binary authors). The PR description's original "Open question" framing has been updated to reflect this.
  • The stale docs/guides/publishing/publish-cargo.md reference in the original PR description was a mistake — the canonical per-type doc is docs/modelcontextprotocol-io/package-types.mdx, and the cargo entry now lives there directly.
  • Two real-world Rust MCP servers queued to register the moment this merges:
    • rust-faf-mcp v0.3.1 — mine, live on crates.io with the visible mcp-name token; end-to-end validated by TestValidateCargo_LivePositivePath in this PR.
    • perfetto-mcp-rs@0xZOne in Support crates.io as a package registry type #1055; will update once they confirm a version bump with the visible token.
  • CI: green, mergeable. 19/19 cargo tests pass (16 existing + 2 hermetic + 1 live anchor). make validate clean. golangci-lint 0 issues.

Ready for review whenever you have a window, @rdimitrov.

@P4ST4S
Copy link
Copy Markdown

P4ST4S commented Jun 1, 2026

All three addressed cleanly. Two specific things stood out:

On the export_test.go pattern: that's the right call. Keeping the exact-baseURL guard on the public ValidateCargo while routing tests through a package-private validateCargoREADME means the integrity check that matters in production isn't relaxed for testability. The test bridge is the right shape, better than the "test-only constructor" alternative I'd hand-waved at.

On the HTML-comment stripping gotcha: that's a genuinely valuable discovery and not one anyone could have predicted from the spec. PyPI and NuGet preserve the raw description; crates.io renders markdown→HTML and strips comments at the rendering layer. Future Rust MCP authors trying the <!-- mcp-name: ... --> pattern from the PyPI docs would hit a silent validator failure with no obvious cause. The package-types.mdx note will save people that exact dead end. The fact that you caught it by publishing two versions back-to-back (v0.3.0 rejected, v0.3.1 accepted) is the kind of end-to-end validation that's hard to fake, the live anchor test in this PR is a real artifact of that work, not a mock.

Re-LGTM from an external reviewer's perspective. The PR is in a tighter state than where it started, the live + hermetic test pair gives strong regression coverage, and the diagnostic split on 5xx vs 403 is a small but real operational win.

Ready for maintainer review whenever, @rdimitrov.

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.

Support crates.io as a package registry type

2 participants