MCP RegistryへGitHub Actionsで自動公開する手順
MCP Registryへのサーバー公開をGitHub Actionsで自動化する手順を、OIDC・GitHub PAT・DNSの3認証方式の使い分けとタグpush運用まで確認します。
MCP Registryへのサーバー公開は、手元でmcp-publisherコマンドを実行する方法に加えて、GitHubのバージョンタグpushをきっかけに自動実行することもできます。公式が用意するワークフローテンプレートはOIDC・GitHub PAT・DNSの3つの認証方式に対応し、npmなど下流のパッケージレジストリへの公開とMCP Registryへの公開を1つのワークフローにまとめられます。この記事では、ワークフローファイルの作成から認証方式の選び方、DNS認証の鍵の用意、タグpushでの実行までを順に扱います。
MCP Registryは公式ページの冒頭で、プレビュー段階だと明記されています。正式提供の前に破壊的変更やデータのリセットが入る可能性があるため、ここで組むワークフローも後から手直しが入りうる前提で読んでください。
自動化の前に確認しておく前提
GitHub Actionsで公開を自動化する前に、次の3点がそろっている必要があります。
- MCPの仕様に沿って実装したサーバーと、公開先のパッケージレジストリ(npm・PyPI・Dockerなど)のアカウント
- サーバーのメタデータを記述した
server.json - パッケージレジストリ側の公開トークン(npmなら
NPM_TOKEN)
公式のサンプルはnpmパッケージを例にしていますが、MCP Registryへの公開手順そのものはパッケージの種類によらず共通です。npm・PyPI・Dockerのどれであっても、「パッケージ本体をレジストリへ公開する」→「mcp-publisherでMCP Registryへメタデータを公開する」という2段構成は変わりません。
手元での公開は、mcp-publisher loginとmcp-publisher publishを自分の端末で毎回実行する作業です。タグpushをトリガーにすると、パッケージ本体の公開とMCP Registryへのメタデータ公開が1つのイベントにまとまり、誰がリリースしても同じ手順が走ります。
ワークフローを組む前に決めておくことが1つあります。認証方式です。認証方式によってサーバー名の名前空間の形式が変わるため、後から変えるとサーバー名を作り直すことになります。
認証方式で決まるサーバー名の形
GitHubベース
名前空間はio.github.ユーザー名またはOrg名/*です。例はio.github.alice/weather-server。GitHub Actionsから公開するなら、まずこちらを選びます。
ドメインベース
名前空間はcom.example.*/*の形で、com.exampleは自分のドメインを逆順にしたものです。例はio.modelcontextprotocol/everything。独自ドメインの名前で公開したいときだけ選びます。
ステップ1: ワークフローファイルを作る
サーバーのリポジトリに.github/workflows/publish-mcp.ymlを作成します。バージョンタグのpushをトリガーにし、パッケージの公開とMCP Registryへの公開を1つのジョブにまとめます。OIDC認証を使う場合の骨格は次のとおりです。
name: Publish to MCP Registry
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # OIDC認証に必須
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "lts/*"
- run: npm ci
- run: npm run test --if-present
- run: npm run build --if-present
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install mcp-publisher
run: |
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
- run: ./mcp-publisher login github-oidc
- run: ./mcp-publisher publishインストール行のURLはreleases/latestを指しているので、ワークフローが走るたびにその時点の最新のmcp-publisherが入ります。この行はテンプレートの記述をそのまま写したものです。
permissions.id-token: writeはOIDC認証だけに必要な設定です。GitHub PATやDNS認証を使う場合は不要になる代わりに、次のステップで扱うシークレットの用意が要ります。認証部分だけを差し替えると、3方式は次のようになります。
# OIDC
- run: ./mcp-publisher login github-oidc
# GitHub PAT
- run: ./mcp-publisher login github --token ${{ secrets.MCP_GITHUB_TOKEN }}
# DNS
- run: ./mcp-publisher login dns --domain example.com --private-key ${{ secrets.MCP_PRIVATE_KEY }}ワークフローの他の部分(チェックアウト・テスト・ビルド・npm publish・mcp-publisherのインストール)は3方式とも共通です。違うのはこの1行だけです。
バージョン番号をタグからserver.jsonへ反映したいときは、publishステップの前に次のようなコマンドを挟めます。テンプレートでは、このステップはコメントアウトされた任意項目です。
VERSION=${GITHUB_REF#refs/tags/v}
jq --arg v "$VERSION" '.version = $v' server.json > server.tmp && mv server.tmp server.jsonserver.jsonのメタデータを更新したいときは、既存バージョンを上書きするのではなく、一意なバージョン文字列を持つserver.jsonを新しく送ります。公開済みのバージョンのメタデータは変更できません(npmと同じ扱いだと公式FAQにあります)。バージョン更新を忘れたままタグだけ進めると、同じバージョン文字列でのpublishになるので注意が必要です。
公開時に独自のメタデータを埋め込みたい場合は、_metaの"io.modelcontextprotocol.registry/publisher-provided"キー配下にJSONを入れておくと、公開後もそのまま保持されます。CIのコミットSHAやビルド日時のようなCI固有の情報を残したいときに使えます。ただしこのフィールドには4KB(4096バイト)というサイズ上限があり、超過するとpublish自体が失敗します。
ステップ2: 認証方式を選び、シークレットを設定する
公式テンプレートの3方式にHTTPを加えた4通りは、必要なシークレットと向いている状況がそれぞれ違います。
| 認証方式 | 追加で必要なシークレット | 向いているケース |
|---|---|---|
| OIDC(推奨) | 追加で必要なシークレットなし | 向いているケースGitHub Actionsから公開する場合の既定選択 |
| GitHub PAT | 追加で必要なシークレットMCP_GITHUB_TOKEN(read:org・read:userスコープ) | 向いているケースGitHubベースの名前空間を、トークンで認証したい場合 |
| DNS | 追加で必要なシークレットMCP_PRIVATE_KEY(Ed25519の秘密鍵) | 向いているケース独自ドメインの名前空間で公開したい場合 |
| HTTP | 追加で必要なシークレット秘密鍵(DNSと同じ鍵形式) | 向いているケースドメイン配下に/.well-known/mcp-registry-authファイルを置ける場合 |
OIDC認証は、MCP Registry用の追加シークレットが要りません。保管するシークレットが1つ減ります。テンプレートでも、OIDCは推奨方式の位置づけです。
手元でmcp-publisher login githubを実行すると、端末に表示されるコードをブラウザで入力する認証フローになります。ワークフローでは人が入力できないため、github-oidcか--tokenに置き換えます。PATを使う場合は、read:orgとread:userの両方のスコープを持つPersonal Access Tokenを発行します。名前はMCP_GITHUB_TOKENにして、リポジトリシークレットに登録します。Managed AgentsのGitHub連携でも、必要最小限のスコープに絞ったトークンをセッション単位のリソースとして渡す設計になっています。セルフホストのGiteaもPersonal Access Token認証で繋げ、手順はGitea MCPサーバーの使い方が扱います。
HTTP認証はDNS認証と同じ鍵ペアの仕組みを使いますが、TXTレコードの代わりにhttps://ドメイン/.well-known/mcp-registry-authというファイルを公開して所有権を証明します。名前空間の形式(com.example.*/*)もDNS認証と同じです。テンプレートに載っているのはOIDC・GitHub PAT・DNSの3方式だけです。HTTP認証を使うなら、次のようなステップを自分で追加します(example.comは自分のドメインに置き換えます)。
- run: ./mcp-publisher login http --domain example.com --private-key ${{ secrets.MCP_PRIVATE_KEY }}どの方式を選んでも、パッケージレジストリ側のトークン(NPM_TOKENなど)は別途必要です。パッケージレジストリの認証とMCP Registryの認証は別物で、片方だけ設定して残りを忘れるミスが起きやすいところです。
DNS認証の鍵を用意する
DNS認証だけは、ワークフローを書く前に手元で鍵とTXTレコードを用意する作業が入ります。手順ページはEd25519のほか、ECDSA P-384、Google Cloud KMS、Azure Key Vaultの鍵を使う方法を載せています。ここではEd25519の流れを、OpenSSL 3.2.1で一時ディレクトリに実行した結果で示します。
DNS認証の準備(Ed25519)
- 1
鍵ペアを作る
openssl genpkey -algorithm Ed25519 -out key.pemで秘密鍵を作ります。生成されたkey.pemの1行目は-----BEGIN PRIVATE KEY-----です。 - 2
公開鍵からTXTレコードを作る
公開鍵のDER形式の末尾32バイトをbase64にして、
v=MCPv1; k=ed25519; p=...の形に組み立てます。この値をDNSのTXTレコードに登録します。 - 3
秘密鍵を16進文字列にする
login dnsの--private-keyには、PEMファイルではなく、鍵本体を16進にした文字列を渡します。この文字列をMCP_PRIVATE_KEYに登録します。
実際に出力される内容は次のとおりです。p=の値は、この記事のために作って破棄した鍵のものです。
MY_DOMAIN="example.com"
PUBLIC_KEY="$(openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64)"
echo "${MY_DOMAIN}. IN TXT \"v=MCPv1; k=ed25519; p=${PUBLIC_KEY}\""
# example.com. IN TXT "v=MCPv1; k=ed25519; p=KN/sUXBO13Xx4+6P/u9YzOznjF8X6LV4Q8h8FbsQ0UI="
PRIVATE_KEY="$(openssl pkey -in key.pem -noout -text | grep -A3 'priv:' | tail -n +2 | tr -d ' :\n')"
echo "${#PRIVATE_KEY}"
# 64Ed25519の秘密鍵は32バイトなので、16進にすると64文字です。長さがこれと食い違うときは、抽出コマンドの途中で切れているか、鍵の種類が違う可能性があります。TXTレコードがDNSに反映されてからlogin dnsを実行します。
秘密鍵そのものはリポジトリにコミットせず、MCP_PRIVATE_KEYのシークレットにだけ入れます。公開鍵側はTXTレコードとして世の中に公開される情報なので、扱いが違います。
ステップ3: バージョンタグをpushして実行する
ワークフローとシークレットの準備ができたら、バージョンタグを作成してpushします。
git tag v1.0.0
git push origin v1.0.0タグのpushで、次の5段階が順に走ります。
タグpushから公開までの流れ
- 1
タグのpushを検知する
on.push.tagsのv*に一致したタグだけがトリガーになります。ブランチへのpushでは動きません。 - 2
テストとビルドを走らせる
npm ciのあと、テストとビルドが続きます。ここで失敗すると、後続の公開は実行されません。 - 3
パッケージレジストリへ公開する
npm publishで、パッケージ本体が下流のレジストリに上がります。 - 4
MCP Registryにログインする
選んだ認証方式の
mcp-publisher loginが実行されます。 - 5
MCP Registryへ公開する
mcp-publisher publishがserver.jsonのメタデータを送ります。
トリガーをブランチへのpushではなくタグのpushにしているのは、開発中のコミットのたびに公開が走るのを防ぐためです。初回は本番用のタグではなく、v0.0.1-testのような検証用タグで一度ワークフロー全体を通す手もあります。ただし検証用タグでもnpm publishとMCP Registryへの公開は実際に走り、MCP Registry側の公開は削除できません。検証用タグの命名は、本番のバージョン体系と混ざらない形にしておきます。
よくあるつまずき
公式が案内しているエラーへの対処は次の2つです。
| エラーメッセージ | 対処 |
|---|---|
| "Authentication failed" | 対処OIDCならid-token: write権限の設定漏れを確認する。PAT・DNSならシークレットの設定を確認する(PATはread:org・read:userスコープ) |
| "Package validation failed" | 対処パッケージレジストリへの公開自体が成功しているか、パッケージ側に必要な検証情報が含まれているかを確認する |
"Package validation failed"の中身は、パッケージの種類ごとに用意された所有権確認の仕組みが満たされていない場合があります。server.jsonに書いたサーバー名と、パッケージ側に埋め込む識別情報が一致していないと失敗します。
パッケージ種別(registryType) | 所有権確認の方法 |
|---|---|
| npm | 所有権確認の方法package.jsonのmcpNameフィールドがserver.jsonのサーバー名と一致すること |
| PyPI | 所有権確認の方法READMEにmcp-name: サーバー名という文字列が含まれること(コメント内でも可) |
| NuGet | 所有権確認の方法READMEに同様のmcp-name: サーバー名文字列が含まれること |
| Cargo | 所有権確認の方法READMEにmcp-name: サーバー名が含まれること(コメントは不可。後述) |
| Docker/OCI | 所有権確認の方法イメージにio.modelcontextprotocol.server.nameアノテーションを付け、サーバー名と一致させること |
| MCPB | 所有権確認の方法配布URLに"mcp"の文字列を含み、server.jsonに成果物のSHA-256(fileSha256)を記載すること |
npmパッケージで自動公開を組む場合、npm publish自体は成功していてもpackage.jsonにmcpNameを書き忘れているだけで"Package validation failed"になります。ワークフローのビルドステップでpackage.jsonとserver.jsonのサーバー名が一致しているかを事前にチェックしておくと、この種の失敗を公開前に検出できます。
Cargo(Rust)だけは、PyPIやNuGetと同じ書き方が通りません。crates.ioはREADMEをHTMLに変換する際にHTMLコメントを取り除くため、<!-- mcp-name: ... -->と書いても検証側から見えません。mcp-name:の文字列は、箇条書きなど目に見える本文として書きます。
MCPBは、fileSha256をMCP Registry自身が検証するわけではありません。ハッシュを検証するのは、インストール時のMCPクライアントです。ハッシュはopenssl dgst -sha256 image-processor.mcpbのように求められます。
対応する公開先にも範囲があります。npmはnpm公開レジストリ(registry.npmjs.org)、PyPIはpypi.org、NuGetはapi.nuget.orgだけです。Docker/OCIの対象は、Docker Hub・ghcr.io・Google Artifact Registry・Azure Container Registry・Microsoft Container Registryです。MCPBはGitHubかGitLabのリリースに置いた成果物が対象になります。社内のプライベートレジストリに置いたパッケージは、このリストの外です。
タグpushのたびに自動実行されるワークフローは、壊れたビルドを誤って本番タグとしてpushするリスクと隣り合わせです。壊れたバージョンを消す手段がない以上、公開ステップの手前にテストとビルドを必ず置きます。--if-presentはスクリプトが無ければ黙って成功扱いにするため、自分のプロジェクトで実際にテストが走ることを確認しておく価値があります。
まとめ
自動公開を組むときは、先に名前空間を決めます。GitHubベースならOIDCで追加シークレットなしに始められ、独自ドメインで公開したい場合だけDNS認証の鍵とTXTレコードを自分で用意することになります。公開したバージョンは取り消せないので、初回のタグpushの前に、サーバー名と所有権確認の記述が一致していることを手元で確かめておくと手戻りを避けられます。
公開前の動作確認にはMCP Inspectorの使い方が使え、MCPそのものの仕組みはMCPとはで解説しています。MCP Registryへの公開とClaude製品への表示は別の仕組みで、Claude.ai・Claude Code・Coworkに表示させたい場合はAnthropic Connectors Directoryとはで扱う提出ポータルからの申請が別途必要です。