Skip to main content

MCP registry

Publish an MCP server manifest to the Model Context Protocol registry

Anodizer publishes a Model Context Protocol server manifest to the public registry at registry.modelcontextprotocol.io, letting MCP-capable clients discover and install your server. The manifest describes how to fetch the server (OCI image, npm tarball, PyPI wheel, NuGet package, or .mcpb bundle) and which transport(s) it speaks. Configured under a top-level mcp: key, mirroring GoReleaser's mcp_registries pipe.

Minimal config

mcp:
  name: io.github.myorg/myapp
  description: "A fast MCP server for managing things"
  packages:
    - registry_type: oci
      identifier: ghcr.io/myorg/myapp
      transport:
        type: stdio

This publishes anonymously (auth.type: none) to the default registry. For server names under the io.github.<owner> namespace you almost always want auth.type: github-oidc so the registry can verify ownership of the GitHub repo. See Authentication.

MCP config fields

FieldTypeDefaultDescription
namestringrequiredFully-qualified server name (e.g. io.github.OWNER/PROJECT). Supports templates
titlestringnoneHuman-readable title shown in registry UIs. Supports templates
descriptionstringnoneOne-line description. Supports templates
homepagestringnoneProject homepage URL. Supports templates
skipstring or boolfalseSkip this publisher. Tera template that evaluates to a truthy value (e.g. "{{ true }}") also skips. Accepts the legacy disable: spelling for back-compat with imported GoReleaser configs
repositoryobjectinferredSource repository metadata. See Repository
packagesobject[]requiredOne or more distribution packages. See Packages
transportsobject[]noneTop-level transport list. Parsed for GoReleaser config-portability (silently ignored — see note below); the current MCP server schema derives transports per-package via packages[].transport, so this list is not emitted to the registry
authobject{type: none}Registry authentication. See Authentication
registrystringhttps://registry.modelcontextprotocol.ioOverride the registry endpoint (for staging or a private mirror)

The top-level retry: config applies: POST /v0/publish calls inherit anodizer's standard retry policy (backoff, max attempts, retryable status codes).

Repository

repository:
  url: https://github.com/myorg/myapp
  source: github          # github | gitlab | gitea
  id: ""                  # optional, source-specific repo ID
  subfolder: ""           # optional, e.g. "servers/myapp" inside a monorepo

All fields support Tera templates. If omitted, anodizer infers url and source from the release context.

Packages

Each entry describes one downloadable form of the server. List all the ones you publish; clients pick the best match for their host.

packages:
  - registry_type: oci            # oci | npm | pypi | nuget | mcpb
    identifier: ghcr.io/myorg/myapp
    transport:
      type: stdio                 # stdio | streamable-http | sse
FieldTypeDescription
registry_typestringOne of oci, npm, pypi, nuget, mcpb
identifierstringPackage coordinate. Supports templates. For OCI: ghcr.io/owner/img. For npm: @scope/name. For PyPI: distribution name. For NuGet: package ID. For mcpb: download URL
transport.typestringstdio, streamable-http, or sse

When registry_type: oci, the published manifest carries an empty version field on the package entry (the registry resolves the image tag itself). Other registry types receive the release version verbatim. This mirrors GoReleaser's mcp_registries behavior.

Top-level transports

transports:
  - type: stdio
  - type: streamable-http

Optional. The transports: list is accepted for GoReleaser config-portability (so a migrated .goreleaser.yaml doesn't error on deny_unknown_fields); the current MCP server schema derives transports per-package via packages[].transport, so this list is not emitted to the registry.

Authentication

auth.type controls how anodizer authenticates the POST /v0/publish call. Default is none.

auth.typeWhen to useToken source
noneServer names without an ownership namespace; private mirrorsNone, or auth.token for a static bearer
githubNames under io.github.<owner>/... published from a workstation or non-GHA CIGitHub PAT in auth.token (or MCP_GITHUB_TOKEN env). Anodizer exchanges it for a short-lived registry token
github-oidcNames under io.github.<owner>/... published from GitHub ActionsGHA-native OIDC id-token (ACTIONS_ID_TOKEN_REQUEST_TOKEN + ACTIONS_ID_TOKEN_REQUEST_URL). Anodizer exchanges it for a short-lived registry token. No PAT required

auth.type: none

mcp:
  name: io.github.myorg/myapp
  auth:
    type: none
  # ...

Used for private registries that gate writes by network or a separate bearer:

mcp:
  registry: https://mcp-mirror.internal.example.com
  auth:
    type: none
    token: "{{ .Env.INTERNAL_MCP_BEARER }}"

auth.type: github (PAT)

mcp:
  name: io.github.myorg/myapp
  auth:
    type: github
    token: "{{ .Env.GITHUB_TOKEN }}"

The PAT only needs read:user scope. Anodizer calls the registry's GitHub-token exchange endpoint and receives a short-lived bearer it uses for POST /v0/publish.

auth.type: github-oidc (GitHub Actions native)

mcp:
  name: io.github.myorg/myapp
  auth:
    type: github-oidc

In a workflow, give the job permission to mint the id-token:

permissions:
  contents: write
  id-token: write    # required for github-oidc

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: tj-smith47/anodizer-action@v1
        with:
          args: release --clean

Anodizer reads the OIDC request URL and token from the standard GHA env vars, requests an id-token scoped to the registry's audience, exchanges it for a short-lived registry bearer, and publishes.

Skipping per release

skip accepts either a bool or a Tera template:

mcp:
  # ...
  skip: "{{ if .Prerelease }}true{{ endif }}"

Common patterns:

GoalValue
Always skipskip: true or skip: "{{ true }}"
Skip pre-releasesskip: "{{ if .Prerelease }}true{{ endif }}"
Skip snapshot buildsskip: "{{ if .IsSnapshot }}true{{ endif }}"

Full example

mcp:
  name: io.github.myorg/myapp
  title: "My MCP Server"
  description: "Provides filesystem and shell tools over MCP"
  homepage: "https://github.com/myorg/myapp"
  repository:
    url: "https://github.com/myorg/myapp"
    source: github
  packages:
    - registry_type: oci
      identifier: ghcr.io/myorg/myapp
      transport:
        type: stdio
    - registry_type: npm
      identifier: "@myorg/myapp-mcp"
      transport:
        type: stdio
  transports:
    - type: stdio
  auth:
    type: github-oidc
  skip: "{{ if .Prerelease }}true{{ endif }}"

Templating

Tera templates are evaluated on these fields before publish: name, title, description, homepage, every repository.* field, every auth.* field, and each packages[].identifier. The standard anodizer template context applies (Version, ProjectName, Env.*, Prerelease, etc.). See Templates.

Dry-run

anodizer release --dry-run renders the full manifest and logs the intended POST without contacting the registry, useful for verifying the package list and transport fields before a real publish.

Migrating from GoReleaser

The top-level YAML key is identical (mcp: in both). GoReleaser's deprecated nested mcp.github: block (from older configs) collapses to top-level mcp.* fields in anodizer — write top-level fields directly. See the GoReleaser migration guide for the full mapping.