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 20); 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 10 minutes." >&2
174 exit 1
175 fi
176 echo "Waiting for nuget.org to index ${RELEASE_VERSION} (attempt ${attempt}/20)..."
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 .