main
yml 192 lines 7.92 KB
Raw
1 name: MCP Registry Release
2
3 on:
4 workflow_dispatch:
5 inputs:
6 hosted_version:
7 description: "Override the version in the hosted listing (e.g. 1.2.6); leave blank to publish it as-is"
8 required: false
9 skip_cli:
10 description: "Publish only the hosted server, leaving the CLI listing alone"
11 type: boolean
12 default: false
13 skip_hosted:
14 description: "Publish only the CLI, leaving the hosted listing alone"
15 type: boolean
16 default: false
17
18 # This repo owns two MCP Registry entries, one file each:
19 #
20 # server.json si.sigit/cli this CLI, a cargo package run as `si mcp`
21 # server-sigit.json si.sigit/sigit the hosted server at sigit.si/api/v1/mcp
22 #
23 # The CLI is what this repo actually ships, so it holds the conventional
24 # filename that `mcp-publisher` defaults to.
25 #
26 # Both sit under one namespace, so one credential covers them. A domain
27 # namespace proves ownership through DNS, not GitHub OIDC: the registry checks
28 # a v=MCPv1 TXT record on sigit.si against a request signed with the matching
29 # Ed25519 private key. See the MCP Registry doc in the sigit-si repo for the
30 # one-time key/DNS setup.
31 permissions:
32 contents: read
33
34 jobs:
35 publish:
36 name: Publish the MCP Registry listings
37 runs-on: ubuntu-latest
38 steps:
39 - name: Checkout
40 uses: actions/checkout@v6
41
42 - name: Take the CLI listing's version from Cargo.toml
43 if: ${{ !inputs.skip_cli }}
44 shell: bash
45 run: |
46 # The workspace manifest is the single source of truth: the listing
47 # names a crates.io version, so a hand-edited number here could point
48 # at a release that doesn't exist.
49 crate_version="$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
50 if [ -z "${crate_version}" ]; then
51 echo "Could not read the workspace version from Cargo.toml." >&2
52 exit 1
53 fi
54 echo "CRATE_VERSION=${crate_version}" >> "$GITHUB_ENV"
55
56 jq --arg v "${crate_version}" \
57 '.version = $v | .packages |= map(.version = $v)' \
58 server.json > server.tmp
59 mv server.tmp server.json
60 cat server.json
61
62 - name: Verify the published crate carries the ownership marker
63 if: ${{ !inputs.skip_cli }}
64 shell: bash
65 run: |
66 # The registry stores metadata only, and validates a package listing
67 # by fetching the package and looking for a marker naming the server.
68 # Checking the working tree isn't enough: a version published before
69 # the marker landed will never carry it, and crates.io versions are
70 # immutable. The comparison is case-sensitive.
71 server_name="$(jq -r '.name' server.json)"
72 crate="$(jq -r '.packages[0].identifier' server.json)"
73
74 for attempt in $(seq 1 20); do
75 if curl -fsSL -H "User-Agent: getsigit/si release-workflow" \
76 "https://crates.io/api/v1/crates/${crate}/${CRATE_VERSION}" >/dev/null; then
77 echo "${crate} ${CRATE_VERSION} is indexed on crates.io."
78 break
79 fi
80 if [ "${attempt}" -eq 20 ]; then
81 echo "${crate} ${CRATE_VERSION} is still not on crates.io after 10 minutes." >&2
82 echo "Publish the crate first: the listing can only name a version that exists." >&2
83 exit 1
84 fi
85 echo "Waiting for crates.io to index ${crate} ${CRATE_VERSION} (attempt ${attempt}/20)..."
86 sleep 30
87 done
88
89 curl -fsSL -H "User-Agent: getsigit/si release-workflow" -o package.crate \
90 "https://crates.io/api/v1/crates/${crate}/${CRATE_VERSION}/download"
91 # `readme` in crates/cli/Cargo.toml points at the repo-root README,
92 # which cargo copies to the crate root when packaging.
93 if ! tar -xOzf package.crate "${crate}-${CRATE_VERSION}/README.md" | grep -q "mcp-name: ${server_name}"; then
94 echo "${crate} ${CRATE_VERSION} README is missing the 'mcp-name: ${server_name}' marker." >&2
95 echo "crates.io versions are immutable — cut a new release with the marker in place." >&2
96 exit 1
97 fi
98 rm package.crate
99
100 - name: Optionally override the version in the hosted listing
101 if: ${{ !inputs.skip_hosted }}
102 shell: bash
103 run: |
104 version="${{ inputs.hosted_version }}"
105 if [ -n "${version}" ]; then
106 jq --arg v "${version}" '.version = $v' server-sigit.json > server-sigit.tmp
107 mv server-sigit.tmp server-sigit.json
108 fi
109 cat server-sigit.json
110
111 - name: Check the hosted remote URL matches the namespace domain
112 if: ${{ !inputs.skip_hosted }}
113 shell: bash
114 run: |
115 # The registry rejects a remote server whose URL isn't under the
116 # namespace's domain. si.sigit -> sigit.si, so every listed remote
117 # must live on sigit.si (or a subdomain).
118 server_name="$(jq -r '.name' server-sigit.json)"
119 domain="$(echo "${server_name%%/*}" | awk -F. '{ for (i=NF; i>=1; i--) printf "%s%s", $i, (i>1 ? "." : "") }')"
120 echo "Server: ${server_name} Domain: ${domain}"
121
122 bad="$(jq -r --arg d "${domain}" '
123 .remotes // []
124 | map(.url | sub("^[a-z]+://"; "") | sub("/.*$"; ""))
125 | map(select(. != $d and (endswith("." + $d) | not)))
126 | .[]' server-sigit.json)"
127 if [ -n "${bad}" ]; then
128 echo "Remote URL host(s) not under ${domain}: ${bad}" >&2
129 exit 1
130 fi
131
132 - name: Install mcp-publisher
133 shell: bash
134 run: |
135 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
136
137 - name: Authenticate to the MCP Registry
138 shell: bash
139 env:
140 MCP_REGISTRY_DNS_PRIVATE_KEY: ${{ secrets.MCP_REGISTRY_DNS_PRIVATE_KEY }}
141 run: |
142 if [ -z "${MCP_REGISTRY_DNS_PRIVATE_KEY}" ]; then
143 echo "MCP_REGISTRY_DNS_PRIVATE_KEY secret is not set. See the MCP Registry doc in the sigit-si repo." >&2
144 exit 1
145 fi
146 ./mcp-publisher login dns --domain=sigit.si --private-key="${MCP_REGISTRY_DNS_PRIVATE_KEY}"
147
148 - name: Publish
149 shell: bash
150 run: |
151 # Republishing a version that is already up is not worth failing the
152 # run over: the other listing may still need to go out.
153 publish_or_skip() {
154 local out
155 out="$(./mcp-publisher publish "$1" 2>&1)" && { echo "${out}"; return 0; }
156 echo "${out}"
157 if echo "${out}" | grep -q "duplicate version"; then
158 echo "$1 is already published at this version — skipping."
159 return 0
160 fi
161 return 1
162 }
163
164 published=0
165 if [ "${{ inputs.skip_cli }}" != "true" ]; then
166 publish_or_skip server.json
167 published=1
168 fi
169 if [ "${{ inputs.skip_hosted }}" != "true" ]; then
170 publish_or_skip server-sigit.json
171 published=1
172 fi
173 if [ "${published}" -eq 0 ]; then
174 echo "Both listings were skipped, so there was nothing to publish." >&2
175 exit 1
176 fi
177
178 - name: Verify the servers are listed
179 shell: bash
180 run: |
181 if [ "${{ inputs.skip_cli }}" != "true" ]; then
182 files="server.json"
183 fi
184 if [ "${{ inputs.skip_hosted }}" != "true" ]; then
185 files="${files} server-sigit.json"
186 fi
187
188 for file in ${files}; do
189 server_name="$(jq -r '.name' "${file}")"
190 echo "== ${server_name} (${file}) =="
191 curl -fsSL "https://registry.modelcontextprotocol.io/v0.1/servers?search=${server_name}" | jq .
192 done