Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ validate-schemas
coverage.out
coverage.html
deploy/infra/infra
registry
registry
.github/instructions*
.vscode/mcp*

50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ The MCP registry provides MCP clients with a list of MCP servers, like an app st
**2025-09-08 update**: The registry has launched in preview 🎉 ([announcement blog post](https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/)). While the system is now more stable, this is still a preview release and breaking changes or data resets may occur. A general availability (GA) release will follow later. We'd love your feedback in [GitHub discussions](https://github.com/modelcontextprotocol/registry/discussions/new?category=ideas) or in the [#registry-dev Discord](https://discord.com/channels/1358869848138059966/1369487942862504016) ([joining details here](https://modelcontextprotocol.io/community/communication)).

Current key maintainers:
- **Adam Jones** (Anthropic) [@domdomegg](https://github.com/domdomegg)

- **Adam Jones** (Anthropic) [@domdomegg](https://github.com/domdomegg)
- **Tadas Antanavicius** (PulseMCP) [@tadasant](https://github.com/tadasant)
- **Toby Padilla** (GitHub) [@toby](https://github.com/toby)

Expand All @@ -24,7 +25,7 @@ Often (but not always) ideas flow through this pipeline:
- **[Issues](https://github.com/modelcontextprotocol/registry/issues)** - Track well-scoped technical work
- **[Pull Requests](https://github.com/modelcontextprotocol/registry/pulls)** - Contribute work towards issues

### Quick start:
### Quick start

#### Pre-requisites

Expand All @@ -41,10 +42,22 @@ make dev-compose

This starts the registry at [`localhost:8080`](http://localhost:8080) with PostgreSQL and seed data. The database uses ephemeral storage and is reset each time you restart the containers, ensuring a clean state for development and testing.

The setup can be configured with environment variables in [docker-compose.yml](./docker-compose.yml) - see [.env.example](./.env.example) for a reference.
#### Alternative: Local setup without Docker

**Prerequisites:**

- PostgreSQL running locally
- Go 1.24.x installed

```bash
# Build and run locally
make build
make dev-local
```

The service runs on [`localhost:8080`](http://localhost:8080) by default. This can be configured with environment variables in `.env` - see [.env.example](./.env.example) for a reference.

<details>
<summary>Alternative: Running a pre-built Docker image</summary>
#### Alternative: Running a pre-built Docker image

Pre-built Docker images are automatically published to GitHub Container Registry:

Expand All @@ -62,12 +75,13 @@ docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:v1.0.0
docker run -p 8080:8080 ghcr.io/modelcontextprotocol/registry:main-20250906-abc123d
```

**Available tags:**
**Available tags:**

- **Releases**: `latest`, `v1.0.0`, `v1.1.0`, etc.
- **Continuous**: `main` (latest main branch build)
- **Development**: `main-<date>-<sha>` (specific commit builds)

</details>

#### Publishing a server

Expand Down Expand Up @@ -100,7 +114,7 @@ For Claude and other AI tools: Always prefer make targets over custom commands w

### Project Structure

```
```text
├── cmd/ # Application entry points
│ └── publisher/ # Server publishing tool
├── data/ # Seed data
Expand All @@ -127,15 +141,35 @@ For Claude and other AI tools: Always prefer make targets over custom commands w
### Authentication

Publishing supports multiple authentication methods:

- **GitHub OAuth** - For publishing by logging into GitHub
- **GitHub OIDC** - For publishing from GitHub Actions
- **DNS verification** - For proving ownership of a domain and its subdomains
- **HTTP verification** - For proving ownership of a domain

The registry validates namespace ownership when publishing. E.g. to publish...:

- `io.github.domdomegg/my-cool-mcp` you must login to GitHub as `domdomegg`, or be in a GitHub Action on domdomegg's repos
- `me.adamjones/my-cool-mcp` you must prove ownership of `adamjones.me` via DNS or HTTP challenge

### Private OCI / GHCR validation

The validator now supports both Docker Hub (`docker.io`) and GitHub Container Registry (`ghcr.io`). For private images supply an auth token through one of:

1. `MCP_REGISTRY_OCI_TOKEN_GHCR_IO=ghp_xxx` (per-host environment variable)
2. `OCI_REGISTRY_AUTH="ghcr.io=ghp_xxx,docker.io=abcdef"` (comma separated mapping)
3. `MCP_REGISTRY_OCI_REGISTRY_AUTH` (same mapping format, lower precedence than the per-host variable)

If running inside GitHub Actions and no explicit token is provided, the `GITHUB_TOKEN` will be used automatically for `ghcr.io`.

Images must include the label:

```text
LABEL io.modelcontextprotocol.server.name="<your-server-name>"
```

Multi-arch images are supported (the first manifest entry is inspected). Validation will skip if rate limited and continue publishing flow, logging a warning.

## More documentation

See the [documentation](./docs) for more details if your question has not been answered here!
41 changes: 40 additions & 1 deletion docs/guides/publishing/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,58 @@ Add your Ed25519 private key as `MCP_PRIVATE_KEY` secret.
## Examples

See these real-world examples of automated publishing workflows:

- [NPM, Docker and MCPB](https://github.com/domdomegg/airtable-mcp-server)
- [NuGet](https://github.com/domdomegg/time-mcp-nuget)
- [PyPI](https://github.com/domdomegg/time-mcp-pypi)

## Tips

You can keep your package version and server.json version in sync automatically with something like:

```yaml
- run: |
VERSION=${GITHUB_REF#refs/tags/v}
jq --arg v "$VERSION" '.version = $v' server.json > tmp && mv tmp server.json
```

## Troubleshooting

- **"Authentication failed"**: Ensure `id-token: write` permission is set for OIDC, or check secrets
- **"Package validation failed"**: Verify your package published to your registry (NPM, PyPi etc.) successfully first, and that you have done the necessary validation steps in the [Publishing Tutorial](publish-server.md)
- **"Package validation failed"**: Verify your package published to your registry (NPM, PyPi etc.) successfully first, and that you have done the necessary validation steps in the [Publishing Tutorial](publish-server.md)

## Container Registry Auth Matrix

When validating Docker/OCI images, the MCP Registry uses the Docker Registry v2 API. For private images, you can provide credentials via environment variables. The validator supports both Docker Hub and GitHub Container Registry (GHCR).

Supported registries:

- Docker Hub: <https://docker.io> (API host <https://registry-1.docker.io>)
- GHCR: <https://ghcr.io>

Environment variables (highest precedence first):

- Per-registry host token
- Docker Hub: `MCP_REGISTRY_OCI_TOKEN_DOCKER_IO="<token>"`
- GHCR: `MCP_REGISTRY_OCI_TOKEN_GHCR_IO="<token>"`

- Mapping variable (comma-separated list of `host=token`)
- `MCP_REGISTRY_OCI_REGISTRY_AUTH="ghcr.io=<token>,docker.io=<token>"`

- GitHub Actions convenience (GHCR only)
- `GITHUB_TOKEN` (ephemeral), used automatically for `ghcr.io` if present

GHCR username for PAT Basic exchange (optional):

- `MCP_REGISTRY_OCI_GHCR_USERNAME="<github-username>"`

Notes:

- For GHCR, if you set `MCP_REGISTRY_OCI_GHCR_USERNAME` and provide a GitHub PAT as token, the validator first exchanges it for a repository-scoped Bearer token. Otherwise the provided token is used directly as Bearer.
- For Docker Hub public images, the validator automatically performs an anonymous token flow; no env vars required.
- For Docker Hub, mapping keys and per-host env variables use the base host `docker.io` (the validator internally talks to the API host `registry-1.docker.io`).

Debugging:

- Set `MCP_REGISTRY_OCI_DEBUG=1` to print non-sensitive debug messages about which auth path is being used (e.g. mapping vs. per-host var, GHCR token exchange, Docker Hub anonymous token).
- To skip label validation during testing, set `MCP_REGISTRY_OCI_SKIP_LABEL_VALIDATION=1`.
31 changes: 31 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
env "github.com/caarlos0/env/v11"
"strings"
)

// Config holds the application configuration
Expand All @@ -17,6 +18,10 @@ type Config struct {
EnableAnonymousAuth bool `env:"ENABLE_ANONYMOUS_AUTH" envDefault:"false"`
EnableRegistryValidation bool `env:"ENABLE_REGISTRY_VALIDATION" envDefault:"true"`

// OCI registry auth: comma separated list of host=token pairs used for validating private images
// Example: "ghcr.io=ghp_xxx,docker.io=abcdef"
OCIRegistryAuth string `env:"OCI_REGISTRY_AUTH" envDefault:""` // Added for parsing OCI registry auth tokens

// OIDC Configuration
OIDCEnabled bool `env:"OIDC_ENABLED" envDefault:"false"`
OIDCIssuer string `env:"OIDC_ISSUER" envDefault:""`
Expand All @@ -37,3 +42,29 @@ func NewConfig() *Config {
}
return &cfg
}

// ParseOCIRegistryAuth converts OCIRegistryAuth string into map[host]token
// Format: "host=token,otherhost=othertoken". Whitespace is trimmed. Invalid entries are ignored.
func (c *Config) ParseOCIRegistryAuth() map[string]string {
authMap := make(map[string]string)
if c == nil || c.OCIRegistryAuth == "" {
return authMap
}
items := strings.Split(c.OCIRegistryAuth, ",")
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
parts := strings.SplitN(item, "=", 2)
if len(parts) != 2 {
continue
}
host := strings.TrimSpace(parts[0])
token := strings.TrimSpace(parts[1])
if host != "" && token != "" {
authMap[host] = token
}
}
return authMap
}
6 changes: 4 additions & 2 deletions internal/importer/importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"

Expand All @@ -19,7 +20,8 @@ import (

func TestImportService_LocalFile(t *testing.T) {
// Create a temporary seed file
tempFile := "/tmp/test_import_seed.json"
tempDir := t.TempDir()
tempFile := filepath.Join(tempDir, "test_import_seed.json")
seedData := []apiv0.ServerJSON{
{
Name: "io.github.test/test-server-1",
Expand Down Expand Up @@ -47,7 +49,7 @@ func TestImportService_LocalFile(t *testing.T) {

err = os.WriteFile(tempFile, jsonData, 0600)
require.NoError(t, err)
defer os.Remove(tempFile)
// no need to remove; t.TempDir handles cleanup

testDB := database.NewTestDB(t)

Expand Down
Loading
Loading