Visitar URL original
feat: add RubyGems (rubygems.org) as a package registry type · modelcontextprotocol/registry@12b0626 · GitHub
Skip to content

Commit 12b0626

Browse files
committed
feat: add RubyGems (rubygems.org) as a package registry type
## Motivation and Context Ruby MCP servers published to rubygems.org cannot be registered through their native package registry today; the only options are an OCI image or an MCPB download. This adds registryType "rubygems", following docs/contributing/add-package-registry.md and mirroring the cargo work (#1207). Design decisions: - Ownership verification reads the gemspec metadata key "mcp_name" from the version-specific rubygems.org API (/api/v2/rubygems/{name}/versions/{version}.json), which covers existence, version, and ownership in a single call. This mirrors the npm validator's manifest-field approach (mcpName in package.json) rather than the README-token approach used for PyPI/NuGet/cargo: rubygems.org has no README API, and gemspec metadata is a first-class, author-controlled field. - The platform is left unspecified in that call so that versions published only as platform-specific builds can be registered. rubygems.org then chooses which build to return, so authors must set mcp_name in every platform build. - A 404 from the version endpoint is disambiguated with a HEAD probe of /api/v1/versions/{name}.json (gem missing vs version missing vs transient), following the npm validator. Yanked versions 404 on the version endpoint (verified against mimemagic 0.3.0); a 200 carrying yanked=true is still rejected defensively. - The documented client invocation is `gem exec --silent --conservative --gem NAME -v VERSION COMMAND`: `--silent` keeps install progress off stdout (required for the stdio transport), and `--conservative` runs offline when the version is already installed. gem exec ships with RubyGems 3.4.8+ (Ruby 3.2.2+), so runtimeHint "gem" is set, unlike cargo which has no single-shot runner. Closes #1582. ## How Has This Been Tested? - Validator tests are hermetic httptest mocks, including the positive path, plus live anchors against rubygems.org negative paths (missing mcp_name, missing gem, yanked version). - `make validate`, `make test-unit`, and `make test-integration` pass, and golangci-lint v2.14.0 reports 0 issues. - The documented invocation was run on RubyGems 4.0.3 from an empty GEM_HOME against an existing MCP server gem (debug-mcp 0.4.0): the first line on stdout is the JSON-RPC initialize response. Without `--silent` the first line is install output instead. With `--conservative` an installed version starts with the network blocked; without it the start fails. A `--` before COMMAND makes gem exec fail, so the documentation tells clients not to insert one. - Not tested: a publish against a real gem carrying mcp_name. I know of no such gem yet (none among the latest versions of 489 MCP-related gems on rubygems.org), so a live positive-path anchor will be added once one is published. ## Breaking Changes None. The change is additive: existing registry types and their validation are untouched.
1 parent 115f852 commit 12b0626

12 files changed

Lines changed: 695 additions & 23 deletions

File tree

‎docs/modelcontextprotocol-io/package-types.mdx‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,70 @@ A Rust-native MCP server for widget operations.
180180

181181
**Cargo-specific gotcha:** Unlike PyPI and NuGet (which preserve HTML comments in their README rendering), **crates.io strips HTML comments during markdown→HTML conversion**. The `<!-- mcp-name: ... -->` hidden-comment form that works for PyPI/NuGet **does not work for cargo** — the token will not appear in the rendered HTML the validator inspects. Cargo authors must include the `mcp-name:` token as visible markdown text. A simple bullet in the Links section is the recommended pattern.
182182

183+
## RubyGems (Ruby) Packages
184+
185+
For RubyGems packages, the MCP Registry currently supports the official rubygems.org registry (`https://rubygems.org`) only.
186+
187+
RubyGems packages use `"registryType": "rubygems"` in `server.json`. For example:
188+
189+
```json server.json highlight={9}
190+
{
191+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
192+
"name": "io.github.username/widget-mcp",
193+
"title": "Widget",
194+
"description": "Ruby-native MCP server",
195+
"version": "1.2.3",
196+
"packages": [
197+
{
198+
"registryType": "rubygems",
199+
"identifier": "widget-mcp",
200+
"version": "1.2.3",
201+
"runtimeHint": "gem",
202+
"transport": {
203+
"type": "stdio"
204+
}
205+
}
206+
]
207+
}
208+
```
209+
210+
### Runtime Model
211+
212+
RubyGems has a single-shot runner: `gem exec`, which ships with RubyGems 3.4.8+ (bundled with Ruby 3.2.2 and later). MCP clients should invoke RubyGems-based servers as:
213+
214+
```
215+
gem exec --silent --conservative --gem <identifier> -v <version> <command> [args]
216+
```
217+
218+
Use the package `identifier` as `<command>`. `gem exec` runs the gem's executable of that name, or the gem's only executable when it is named differently. A gem with several executables, none named after the gem, cannot be started this way, so name the server's executable after the gem.
219+
220+
Both flags matter:
221+
222+
- `--silent` keeps RubyGems output (such as first-run install progress) off stdout. Without it, install output corrupts the stdio transport's JSON-RPC handshake. `--quiet` is not enough: it still prints "Building native extensions" to stdout. Note that `--silent` also hides RubyGems errors: a missing gem or version exits non-zero with nothing on stderr, so rerun without it to diagnose a failed start.
223+
- `--conservative` uses an already-installed matching version and runs fully offline; without it, every invocation performs remote dependency resolution against rubygems.org and fails when offline.
224+
225+
Options must precede the command token; everything after it is passed to the server. Do not insert `--` before the command: the `gem` CLI treats what follows `--` as build arguments, so `gem exec` fails with "Please specify an executable to run". The example above sets `"runtimeHint": "gem"` accordingly.
226+
227+
Two behaviors to be aware of:
228+
229+
- Since RubyGems 3.5.19, `gem exec` installs the gem and its dependencies into the user's regular `GEM_HOME`. There is no isolated store, and RubyGems provides no cleanup mechanism for these installs.
230+
- A pre-existing Ruby >= 3.2.2 is required (or an older Ruby whose RubyGems has been updated to 3.4.8+). Unlike `npx` (Node.js bundles npm) or `uvx` (uv can provision Python), there is no mechanism to provision Ruby itself.
231+
232+
### Ownership Verification
233+
234+
The MCP Registry verifies ownership of RubyGems packages by checking the gemspec metadata for an `mcp_name` entry. Its value **MUST** match the server name from `server.json`. Add one line to your gemspec and publish:
235+
236+
```ruby widget-mcp.gemspec highlight={5}
237+
Gem::Specification.new do |spec|
238+
spec.name = "widget-mcp"
239+
spec.version = "1.2.3"
240+
241+
spec.metadata["mcp_name"] = "io.github.username/widget-mcp"
242+
end
243+
```
244+
245+
The registry reads this from the rubygems.org version API (`/api/v2/rubygems/{name}/versions/{version}.json`), so no README changes are needed. If a version is published for several platforms, set `mcp_name` in every platform build: rubygems.org chooses which build that API returns. The README-token approach used for PyPI, NuGet, and cargo is not applicable to RubyGems: rubygems.org has no README API, and gemspec metadata is the first-class, author-controlled field for this kind of information.
246+
183247
## Docker/OCI Images
184248

185249
For Docker/OCI images, the MCP Registry currently supports:

‎docs/reference/api/openapi.yaml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,11 +664,12 @@ components:
664664
properties:
665665
registryType:
666666
type: string
667-
description: Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'cargo', 'oci', 'nuget', 'mcpb')
667+
description: Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'cargo', 'rubygems', 'oci', 'nuget', 'mcpb')
668668
examples:
669669
- "npm"
670670
- "pypi"
671671
- "cargo"
672+
- "rubygems"
672673
- "oci"
673674
- "nuget"
674675
- "mcpb"
@@ -684,6 +685,7 @@ components:
684685
- "https://api.nuget.org/v3/index.json"
685686
- "https://github.com"
686687
- "https://gitlab.com"
688+
- "https://rubygems.org"
687689
identifier:
688690
type: string
689691
description: Package identifier - either a package name (for registries) or URL (for direct downloads)
@@ -706,7 +708,7 @@ components:
706708
runtimeHint:
707709
type: string
708710
description: A hint to help clients determine the appropriate runtime for the package. This field should be provided when `runtimeArguments` are present.
709-
examples: [npx, uvx, docker, dnx]
711+
examples: [npx, uvx, docker, dnx, gem]
710712
transport:
711713
$ref: '#/components/schemas/LocalTransport'
712714
description: Transport protocol configuration for the package

‎docs/reference/server-json/draft/server.schema.json‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,17 +243,19 @@
243243
"https://docker.io",
244244
"https://api.nuget.org/v3/index.json",
245245
"https://github.com",
246-
"https://gitlab.com"
246+
"https://gitlab.com",
247+
"https://rubygems.org"
247248
],
248249
"format": "uri",
249250
"type": "string"
250251
},
251252
"registryType": {
252-
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'cargo', 'oci', 'nuget', 'mcpb')",
253+
"description": "Registry type indicating how to download packages (e.g., 'npm', 'pypi', 'cargo', 'rubygems', 'oci', 'nuget', 'mcpb')",
253254
"examples": [
254255
"npm",
255256
"pypi",
256257
"cargo",
258+
"rubygems",
257259
"oci",
258260
"nuget",
259261
"mcpb"
@@ -273,7 +275,8 @@
273275
"npx",
274276
"uvx",
275277
"docker",
276-
"dnx"
278+
"dnx",
279+
"gem"
277280
],
278281
"type": "string"
279282
},

‎docs/reference/server-json/generic-server-json.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,36 @@ The same `registryType` / `identifier` pattern works for other supported OCI hos
401401
}
402402
```
403403

404+
### RubyGems (Ruby) Package Example
405+
406+
RubyGems 3.4.8+ (bundled with Ruby 3.2.2+) provides `gem exec` as the single-shot runner. MCP clients invoke the server as `gem exec --silent --conservative --gem <identifier> -v <version> <command> [args]`; `--silent` keeps install progress off stdout (required for the stdio transport) and `--conservative` runs fully offline when a matching version is already installed.
407+
408+
```json
409+
{
410+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
411+
"name": "io.github.example/widget-mcp",
412+
"description": "Ruby-native MCP server",
413+
"title": "Widget",
414+
"repository": {
415+
"url": "https://github.com/example/widget-mcp",
416+
"source": "github"
417+
},
418+
"version": "1.2.3",
419+
"packages": [
420+
{
421+
"registryType": "rubygems",
422+
"registryBaseUrl": "https://rubygems.org",
423+
"identifier": "widget-mcp",
424+
"version": "1.2.3",
425+
"runtimeHint": "gem",
426+
"transport": {
427+
"type": "stdio"
428+
}
429+
}
430+
]
431+
}
432+
```
433+
404434
### NuGet (.NET) Package Example
405435

406436
The `dnx` tool ships with the .NET 10 SDK, starting with Preview 6.

‎docs/reference/server-json/official-registry-requirements.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Only trusted public registries are supported. Private registries and alternative
3434
- **PyPI**: `https://pypi.org` only
3535
- **NuGet**: `https://api.nuget.org/v3/index.json` only
3636
- **Cargo**: `https://crates.io` only
37+
- **RubyGems**: `https://rubygems.org` only
3738
- **Docker/OCI**:
3839
- Docker Hub (`docker.io`)
3940
- GitHub Container Registry (`ghcr.io`)

‎internal/validators/package.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ func ValidatePackage(ctx context.Context, pkg model.Package, serverName string)
2525
return registries.ValidateMCPB(ctx, pkg, serverName)
2626
case model.RegistryTypeCargo:
2727
return registries.ValidateCargo(ctx, pkg, serverName)
28+
case model.RegistryTypeRubyGems:
29+
return registries.ValidateRubyGems(ctx, pkg, serverName)
2830
default:
2931
return fmt.Errorf("unsupported registry type: %s", pkg.RegistryType)
3032
}

‎internal/validators/registries/export_test.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ var (
1818
ValidatePyPIPackage = validatePyPIPackage
1919
ValidateNPMPackage = validateNPMPackage
2020
)
21+
22+
// ValidateRubyGemsPackage exposes the package-private validateRubyGemsPackage to
23+
// the external _test package so httptest-driven tests can exercise the metadata
24+
// fetch and status-disambiguation pipeline (missing gem vs missing/unpropagated
25+
// version vs transient upstream) against a mock server, bypassing the
26+
// exact-baseURL guard that the public ValidateRubyGems enforces.
27+
var ValidateRubyGemsPackage = validateRubyGemsPackage

0 commit comments

Comments
 (0)