development
claude/smbcloud-github-app-k9641p
development
feature/apps
feature/build-release
feature/ci-docs
feature/ci-ruby-gems
feature/cicd
feature/completion
feature/dist-nuget
feature/fix-ci-sdk-pypi-rubygem
feature/fix-clippy-on-stable-channel
feature/fix-deployment-script
feature/github-email
feature/gresiq-v1
feature/project-new-structure
feature/rsync-deploy
feature/ruby-runner
feature/sdk-npm
feature/sdk-workflows
feature/smbcloud-cli-release-npm
feature/smbcloud-deploy
feature/wasm
main
release/v0.7.4
| 1 | name: MCP Registry Release |
| 2 | |
| 3 | on: |
| 4 | workflow_dispatch: |
| 5 | inputs: |
| 6 | tag: |
| 7 | description: "Release tag (e.g. v0.4.12)" |
| 8 | required: true |
| 9 | |
| 10 | # OIDC is how we authenticate to the MCP Registry: the registry trusts a GitHub |
| 11 | # Actions token issued for this repository, so no long-lived secret is needed. |
| 12 | permissions: |
| 13 | id-token: write |
| 14 | contents: read |
| 15 | |
| 16 | jobs: |
| 17 | publish: |
| 18 | name: Publish MCP servers to the MCP Registry |
| 19 | runs-on: ubuntu-latest |
| 20 | steps: |
| 21 | - name: Checkout |
| 22 | uses: actions/checkout@v6 |
| 23 | with: |
| 24 | ref: ${{ github.event.inputs.tag || github.ref }} |
| 25 | |
| 26 | - name: Set the release version |
| 27 | shell: bash |
| 28 | run: | |
| 29 | release_version="${{ github.event.inputs.tag }}" |
| 30 | release_version="${release_version#v}" |
| 31 | |
| 32 | echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV" |
| 33 | |
| 34 | - name: Check the server metadata is consistent |
| 35 | shell: bash |
| 36 | run: | |
| 37 | server_name="$(jq -r '.name' server.json)" |
| 38 | echo "Server name: ${server_name}" |
| 39 | |
| 40 | remote_url="https://api.smbcloud.xyz/v1/mcp" |
| 41 | if ! jq -e --arg url "${remote_url}" \ |
| 42 | 'any(.remotes[]?; .type == "streamable-http" and .url == $url)' \ |
| 43 | server.json >/dev/null; then |
| 44 | echo "server.json is missing the Streamable HTTP remote ${remote_url}." >&2 |
| 45 | exit 1 |
| 46 | fi |
| 47 | echo "MCP_REMOTE_URL=${remote_url}" >> "$GITHUB_ENV" |
| 48 | |
| 49 | # npm proves ownership through `mcpName` in the published package.json, |
| 50 | # which is generated by this script at release time. |
| 51 | grep -q "mcpName: \"${server_name}\"" npm/scripts/render-main-package.cjs |
| 52 | |
| 53 | # NuGet proves ownership through an `mcp-name:` marker in the package README. |
| 54 | grep -q "mcp-name: ${server_name}" nuget/smbcloud-cli/README.md |
| 55 | |
| 56 | xcrs_server_name="$(jq -r '.name' server-xcrs.json)" |
| 57 | grep -q "mcp-name: ${xcrs_server_name}" crates/xcrs/README.md |
| 58 | |
| 59 | - name: Verify the hosted MCP endpoint |
| 60 | shell: bash |
| 61 | run: | |
| 62 | remote_url="${MCP_REMOTE_URL}" |
| 63 | response_headers="$(mktemp)" |
| 64 | response_body="$(mktemp)" |
| 65 | trap 'rm -f "${response_headers}" "${response_body}"' EXIT |
| 66 | |
| 67 | request_body='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smbcloud-registry-release-check","version":"1"}}}' |
| 68 | if ! http_status="$(curl --http1.1 --silent --show-error \ |
| 69 | --connect-timeout 10 \ |
| 70 | --max-time 20 \ |
| 71 | --retry 2 \ |
| 72 | --retry-all-errors \ |
| 73 | --retry-delay 2 \ |
| 74 | --dump-header "${response_headers}" \ |
| 75 | --output "${response_body}" \ |
| 76 | --write-out '%{http_code}' \ |
| 77 | --header 'Accept: application/json, text/event-stream' \ |
| 78 | --header 'Content-Type: application/json' \ |
| 79 | --data "${request_body}" \ |
| 80 | "${remote_url}")"; then |
| 81 | echo "Failed to reach the hosted MCP endpoint ${remote_url}." >&2 |
| 82 | exit 1 |
| 83 | fi |
| 84 | |
| 85 | if [ "${http_status}" = "401" ]; then |
| 86 | if ! grep -Eiq '^www-authenticate:[[:space:]]*Bearer([[:space:]]|$)' "${response_headers}"; then |
| 87 | echo "The hosted MCP endpoint returned 401 without a Bearer challenge." >&2 |
| 88 | cat "${response_headers}" >&2 |
| 89 | exit 1 |
| 90 | fi |
| 91 | echo "The hosted MCP endpoint returned the expected authentication challenge." |
| 92 | elif [ "${http_status}" = "200" ]; then |
| 93 | if jq -e \ |
| 94 | '.jsonrpc == "2.0" and .id == 1 and (has("result") or has("error"))' \ |
| 95 | "${response_body}" >/dev/null 2>&1 || \ |
| 96 | sed -n 's/^data:[[:space:]]*//p' "${response_body}" | \ |
| 97 | jq -se 'any(.[]; .jsonrpc == "2.0" and .id == 1 and (has("result") or has("error")))' \ |
| 98 | >/dev/null; then |
| 99 | echo "The hosted MCP endpoint returned a valid initialize response." |
| 100 | else |
| 101 | echo "The hosted MCP endpoint returned HTTP 200 without a valid MCP response." >&2 |
| 102 | cat "${response_body}" >&2 |
| 103 | exit 1 |
| 104 | fi |
| 105 | else |
| 106 | echo "The hosted MCP endpoint returned unexpected HTTP status ${http_status}." >&2 |
| 107 | cat "${response_headers}" >&2 |
| 108 | cat "${response_body}" >&2 |
| 109 | exit 1 |
| 110 | fi |
| 111 | |
| 112 | - name: Set release versions in server metadata |
| 113 | shell: bash |
| 114 | run: | |
| 115 | jq --arg v "${RELEASE_VERSION}" \ |
| 116 | '.version = $v | .packages |= map(.version = $v)' \ |
| 117 | server.json > server.tmp |
| 118 | mv server.tmp server.json |
| 119 | |
| 120 | xcrs_version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/xcrs/Cargo.toml | head -n 1)" |
| 121 | if [ "${xcrs_version}" != "${RELEASE_VERSION}" ]; then |
| 122 | echo "xcrs version ${xcrs_version} does not match release version ${RELEASE_VERSION}." >&2 |
| 123 | exit 1 |
| 124 | fi |
| 125 | jq --arg v "${xcrs_version}" \ |
| 126 | '.version = $v | .packages |= map(.version = $v) | .description |= if length > 100 then .[0:97] + "..." else . end' \ |
| 127 | server-xcrs.json > server-xcrs.tmp |
| 128 | mv server-xcrs.tmp server-xcrs.json |
| 129 | cat server.json |
| 130 | cat server-xcrs.json |
| 131 | |
| 132 | - name: Verify the published packages carry the ownership marker |
| 133 | shell: bash |
| 134 | run: | |
| 135 | # The registry stores metadata only, and validates it by fetching each |
| 136 | # listed package and looking for a marker naming this server. Checking |
| 137 | # the sources isn't enough: a package published before the marker |
| 138 | # landed will still be missing it. Both comparisons are case-sensitive. |
| 139 | server_name="$(jq -r '.name' server.json)" |
| 140 | |
| 141 | # npm is queryable within seconds of publishing, but the npm release |
| 142 | # workflow runs in parallel with the NuGet one that triggers this, so |
| 143 | # it may still be building its six platform packages. |
| 144 | for attempt in $(seq 1 20); do |
| 145 | if npm view "@smbcloud/cli@${RELEASE_VERSION}" version; then |
| 146 | break |
| 147 | fi |
| 148 | if [ "${attempt}" -eq 20 ]; then |
| 149 | echo "@smbcloud/cli@${RELEASE_VERSION} is still not on npm after 10 minutes." >&2 |
| 150 | exit 1 |
| 151 | fi |
| 152 | echo "Waiting for @smbcloud/cli@${RELEASE_VERSION} on npm (attempt ${attempt}/20)..." |
| 153 | sleep 30 |
| 154 | done |
| 155 | |
| 156 | npm_mcp_name="$(npm view "@smbcloud/cli@${RELEASE_VERSION}" mcpName)" |
| 157 | if [ "${npm_mcp_name}" != "${server_name}" ]; then |
| 158 | echo "@smbcloud/cli@${RELEASE_VERSION} declares mcpName '${npm_mcp_name}', expected '${server_name}'." >&2 |
| 159 | echo "npm versions are immutable — cut a new release with the marker in place." >&2 |
| 160 | exit 1 |
| 161 | fi |
| 162 | |
| 163 | # nuget.org validates a package before it reaches the flat container, |
| 164 | # so a version pushed minutes ago is not queryable yet. Poll instead |
| 165 | # of failing the release on that race. |
| 166 | for attempt in $(seq 1 60); do |
| 167 | nuget_versions="$(curl -fsSL "https://api.nuget.org/v3-flatcontainer/smbcloud.cli/index.json")" |
| 168 | if echo "${nuget_versions}" | jq -e --arg v "${RELEASE_VERSION}" '.versions | index($v)' >/dev/null; then |
| 169 | echo "SmbCloud.Cli ${RELEASE_VERSION} is indexed." |
| 170 | break |
| 171 | fi |
| 172 | if [ "${attempt}" -eq 20 ]; then |
| 173 | echo "SmbCloud.Cli ${RELEASE_VERSION} is still not indexed on nuget.org after 30 minutes." >&2 |
| 174 | exit 1 |
| 175 | fi |
| 176 | echo "Waiting for nuget.org to index ${RELEASE_VERSION} (attempt ${attempt}/60)..." |
| 177 | sleep 30 |
| 178 | done |
| 179 | |
| 180 | curl -fsSL -o package.nupkg \ |
| 181 | "https://api.nuget.org/v3-flatcontainer/smbcloud.cli/${RELEASE_VERSION}/smbcloud.cli.${RELEASE_VERSION}.nupkg" |
| 182 | if ! unzip -p package.nupkg README.md | grep -q "mcp-name: ${server_name}"; then |
| 183 | echo "SmbCloud.Cli ${RELEASE_VERSION} README is missing the 'mcp-name: ${server_name}' marker." >&2 |
| 184 | exit 1 |
| 185 | fi |
| 186 | rm package.nupkg |
| 187 | |
| 188 | xcrs_server_name="$(jq -r '.name' server-xcrs.json)" |
| 189 | xcrs_version="$(jq -r '.version' server-xcrs.json)" |
| 190 | for attempt in $(seq 1 20); do |
| 191 | if curl -fsSL -H "User-Agent: smbcloudXYZ/smbcloud-cli release-workflow" "https://crates.io/api/v1/crates/xcrs/${xcrs_version}" >/dev/null; then |
| 192 | echo "xcrs ${xcrs_version} is indexed on crates.io." |
| 193 | break |
| 194 | fi |
| 195 | if [ "${attempt}" -eq 20 ]; then |
| 196 | echo "xcrs ${xcrs_version} is still not indexed on crates.io after 10 minutes." >&2 |
| 197 | exit 1 |
| 198 | fi |
| 199 | echo "Waiting for crates.io to index xcrs ${xcrs_version} (attempt ${attempt}/20)..." |
| 200 | sleep 30 |
| 201 | done |
| 202 | |
| 203 | curl -fsSL -H "User-Agent: smbcloudXYZ/smbcloud-cli release-workflow" -o xcrs.crate \ |
| 204 | "https://crates.io/api/v1/crates/xcrs/${xcrs_version}/download" |
| 205 | if ! tar -xOf xcrs.crate "xcrs-${xcrs_version}/README.md" | grep -q "mcp-name: ${xcrs_server_name}"; then |
| 206 | echo "xcrs ${xcrs_version} README is missing the 'mcp-name: ${xcrs_server_name}' marker." >&2 |
| 207 | exit 1 |
| 208 | fi |
| 209 | rm xcrs.crate |
| 210 | |
| 211 | - name: Install mcp-publisher |
| 212 | shell: bash |
| 213 | run: | |
| 214 | 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 |
| 215 | |
| 216 | - name: Authenticate to the MCP Registry |
| 217 | shell: bash |
| 218 | run: ./mcp-publisher login github-oidc |
| 219 | |
| 220 | - name: Publish |
| 221 | shell: bash |
| 222 | run: | |
| 223 | publish_or_skip() { |
| 224 | local out |
| 225 | out="$(./mcp-publisher publish 2>&1)" && { echo "${out}"; return 0; } |
| 226 | echo "${out}" |
| 227 | if echo "${out}" | grep -q "duplicate version"; then |
| 228 | echo "Version already published — skipping." |
| 229 | return 0 |
| 230 | fi |
| 231 | return 1 |
| 232 | } |
| 233 | publish_or_skip |
| 234 | cp server-xcrs.json server.json |
| 235 | publish_or_skip |
| 236 | |
| 237 | - name: Verify the server is listed |
| 238 | shell: bash |
| 239 | run: | |
| 240 | server_name="$(jq -r '.name' server.json)" |
| 241 | curl -fsSL "https://registry.modelcontextprotocol.io/v0.1/servers?search=${server_name}" | jq . |
| 242 | xcrs_server_name="$(jq -r '.name' server-xcrs.json)" |
| 243 | curl -fsSL "https://registry.modelcontextprotocol.io/v0.1/servers?search=${xcrs_server_name}" | jq . |