feat: add cargo (crates.io) as a package registry type#1207
Conversation
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
26eb180 to
6b2b006
Compare
|
@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. |
|
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
The current 16 test cases all exercise rejection paths. None of them reach the final This is solvable today without waiting for a live 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 Worth noting: 5xx surfaces as "not found" The check PyPI's validator has the same shape ( Minor: 2-call doc comment is precise but slightly incomplete The The design choices I particularly liked:
Nice work. |
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.
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).
|
@P4ST4S — thanks for the careful read and the live API verification. All three items addressed in 1f3be53: Mildly blocking — positive-path test coverage
Defer-able — 5xx surfaces as "not found" Minor — doc comment precision Cargo-specific gotcha discovered while testing live (and addressed in crates.io strips HTML comments during markdown→HTML README rendering — unlike PyPI/NuGet, which preserve them in their raw description payloads. So the Caught this by shipping
@rdimitrov — quick state-of-the-PR summary:
Ready for review whenever you have a window, @rdimitrov. |
|
All three addressed cleanly. Two specific things stood out: On the 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 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. |
Adds support for
registryType: cargoso Rust MCP servers published to crates.io can be registered through thedocumented 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 inAdditional context is settled.
How Has This Been Tested?
make validate— clean. Schema-vs-openapi sync verified; 17/17 examples ingeneric-server-json.mdvalidate against the schema;
expectedServerJSONCountbumped 16 → 17 to match the new Cargo example.go test ./internal/validators/...— passes. 16 cargo validator sub-cases across 4 test functions, runagainst real crates.io (~2.5s wall):
serde,tokio,rand) — all correctly rejected for missingmcp-nametokenio.github.OWNER/REPO, multi-hyphen, underscore, numeric suffix)rust-faf-mcpv0.2.3+ being published withmcp-name: io.github.Wolfe-Jam/rust-faf-mcpin its README. Reserved as a TODO incargo_test.go; uncomments to become thelive anchor once that publish lands.
make test-unit— not run due to a Go toolchain version quirk on my workstation (projectauto-downloads 1.26.0; my local
gois 1.25.6, causing compile mismatch). CI will exercise the fullPostgreSQL-backed suite.
Breaking Changes
None. This is an additive change —
cargojoins the supportedregistryTypeenum alongsidenpm/pypi/nuget/oci/mcpb. Existing publishes continue to work unchanged.Types of changes
generic-server-json.md)Checklist
Additional context
Validator design — two-call retrieval on the documented API
internal/validators/registries/cargo.gomirrors the PyPI validator's README-token approach (substring-matchmcp-name: <serverName>), with a two-call retrieval pattern to stay on the documented public crates.io API:GET /api/v1/crates/{name}/{version}/readmereturns 200 OK with a JSON pointer ({"url": "https://static.crates.io/readmes/.../...html"}) — crates.io hands us the URL rather than emitting a 302.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 installis one-time (binary lands onPATH at
~/.cargo/bin), not per-invocation likenpx,uvx, ordnx(the new one in .NET 10 SDK Preview 6+).The new Cargo example in
generic-server-json.mdannotates this honestly and omitsruntimeHint.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 aboveis settled.
Cargo.tomlautodetect incmd/publisher/commands/init.go— separate concern, separate PR.rust-faf-mcpv0.2.3+ publishing as noted in How Has This BeenTested?.