@sigit / si / commits / fb859ce

Split the MCP Registry listing into a CLI crate listing plus a hosted one

server.json was a remote Streamable-HTTP listing for sigit.si/api/v1/mcp, but that's the hosted server's entry, not this repo's: this repo ships the CLI. Give the hosted entry its own file named after its leaf (server-sigit.json), and turn server.json into the cargo/stdio listing for the si CLI (si.sigit/cli) that mcp-publisher publishes by default. The release workflow now publishes both listings and takes the CLI's version from Cargo.toml, so the listing can only name a crates.io version that exists; it also verifies the published crate's README carries the mcp-name marker the registry looks for, and waits for crates.io indexing. Bump the workspace to 1.1.0 so the listing has a version to name. Co-Authored-By: siGit Code <noreply@sigit.si>

Seto Elkahfi committed Sep 4, 2026 at 08:58 UTC fb859ce0fd57226790d0ec03e55bd86436f93a50
7 files changed +251 -42
.github/workflows/release-mcp-registry.yml
+132 -19
@@ -3,42 +3,119 @@ name: MCP Registry Release
3 on:
4 workflow_dispatch:
5 inputs:
6 - version:
7 - description: "Override the version in server.json (e.g. 0.2.0); leave blank to publish as-is"
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
10 -# A remote server proves namespace ownership through domain (DNS) verification,
11 -# not GitHub OIDC: the registry checks a v=MCPv1 TXT record on sigit.si against
12 -# a request signed with the matching Ed25519 private key. See the MCP Registry
13 -# doc in the sigit-si repo for the one-time key/DNS setup.
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:
19 - name: Publish server.json to the MCP Registry
36 + name: Publish the MCP Registry listings
37 runs-on: ubuntu-latest
38 steps:
39 - name: Checkout
40 uses: actions/checkout@v6
41
25 - - name: Optionally override the version in server.json
42 + - name: Take the CLI listing's version from Cargo.toml
43 + if: ${{ !inputs.skip_cli }}
44 shell: bash
45 run: |
28 - version="${{ github.event.inputs.version }}"
29 - if [ -n "${version}" ]; then
30 - jq --arg v "${version}" '.version = $v' server.json > server.tmp
31 - mv server.tmp server.json
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
35 - - name: Check the remote URL matches the namespace domain
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).
41 - server_name="$(jq -r '.name' server.json)"
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
@@ -46,7 +123,7 @@ jobs:
123 .remotes // []
124 | map(.url | sub("^[a-z]+://"; "") | sub("/.*$"; ""))
125 | map(select(. != $d and (endswith("." + $d) | not)))
49 - | .[]' server.json)"
126 + | .[]' server-sigit.json)"
127 if [ -n "${bad}" ]; then
128 echo "Remote URL host(s) not under ${domain}: ${bad}" >&2
129 exit 1
@@ -70,10 +147,46 @@ jobs:
147
148 - name: Publish
149 shell: bash
73 - run: ./mcp-publisher publish
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
75 - - name: Verify the server is listed
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: |
78 - server_name="$(jq -r '.name' server.json)"
79 - curl -fsSL "https://registry.modelcontextprotocol.io/v0.1/servers?search=${server_name}" | jq .
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
Cargo.lock
+2 -2
@@ -1491,7 +1491,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
1491
1492 [[package]]
1493 name = "sigit-si-api"
1494 -version = "1.0.0"
1494 +version = "1.1.0"
1495 dependencies = [
1496 "chrono",
1497 "reqwest",
@@ -1503,7 +1503,7 @@ dependencies = [
1503
1504 [[package]]
1505 name = "sigit-si-cli"
1506 -version = "1.0.0"
1506 +version = "1.1.0"
1507 dependencies = [
1508 "anyhow",
1509 "chrono",
Cargo.toml
+2 -2
@@ -7,7 +7,7 @@ members = ["crates/cli", "crates/sigit-si-api"]
7 # on without pulling in clap, dialoguer, and the rest of the terminal surface.
8 # `cli` is the thin `si` binary on top of it.
9 [workspace.package]
10 -version = "1.0.0"
10 +version = "1.1.0"
11 edition = "2021"
12 authors = ["Seto Elkahfi <seto@sigit.si>"]
13 license = "Apache-2.0"
@@ -36,7 +36,7 @@ rmcp = { version = "3.2", features = [
36 schemars = "1"
37 serde = { version = "1", features = ["derive"] }
38 serde_json = "1"
39 -sigit-si-api = { version = "1.0.0", path = "crates/sigit-si-api" }
39 +sigit-si-api = { version = "1.1.0", path = "crates/sigit-si-api" }
40 tabled = { version = "0.20", default-features = false, features = ["std"] }
41 thiserror = "2.0"
42 # `io-std` is what the MCP stdio transport binds to.
README.md
+26 -10
@@ -42,6 +42,8 @@ Pass `--json` to any read command for machine-readable output.
42
43 ### As an MCP server
44
45 +<!-- mcp-name: si.sigit/cli -->
46 +
47 `si mcp` serves these same commands to an AI agent as
48 [MCP](https://modelcontextprotocol.io) tools over stdio:
49
@@ -94,16 +96,30 @@ they're served over the same MCP (Model Context Protocol) JSON-RPC endpoint
96 the AI agent uses. `sigit-si-api` speaks that transport internally; it's not
97 visible at the call site.
98
97 -## MCP Registry listing
98 -
99 -The sigit.si MCP server (the same endpoint `sigit-si-api` talks to, and the
100 -official MCP server the siGit Code CLI bakes in) is published to the
101 -[official MCP Registry](https://registry.modelcontextprotocol.io) as
102 -**`si.sigit/sigit`**, a remote Streamable-HTTP listing, so registry-aware
103 -clients can add it in one click. `server.json` at the repo root and the
104 -`release-mcp-registry.yml` workflow own that listing; namespace ownership is
105 -proven by a DNS TXT record on `sigit.si`. The internal setup and release
106 -runbook live with the server (private `sigit-si` repo).
99 +## MCP Registry listings
100 +
101 +This repo publishes two entries to the
102 +[official MCP Registry](https://registry.modelcontextprotocol.io), one per way
103 +of reaching the same tools, so registry-aware clients can add either in one
104 +click:
105 +
106 +| Listing | File | What it is |
107 +| --- | --- | --- |
108 +| `si.sigit/cli` | `server.json` | This CLI: a cargo package listing that runs `si mcp` over stdio. |
109 +| `si.sigit/sigit` | `server-sigit.json` | The hosted server at `sigit.si/api/v1/mcp`, a remote Streamable-HTTP listing. The endpoint `sigit-si-api` talks to, and the official server siGit Code bakes in. |
110 +
111 +The CLI holds `server.json` because it's what this repo ships, and that's the
112 +filename `mcp-publisher` defaults to. Sibling listings are named after their
113 +own leaf, so the file says which entry it publishes.
114 +
115 +Both go out through the `release-mcp-registry.yml` workflow, and both prove
116 +namespace ownership with a DNS TXT record on `sigit.si` rather than GitHub
117 +OIDC, so one credential covers the pair. A package listing has a second hurdle:
118 +the registry fetches the crate from crates.io and looks for the
119 +`mcp-name: si.sigit/cli` marker in its README, which is why that marker sits in
120 +this file. Crate versions are immutable, so a release that ships without the
121 +marker can never be listed, and fixing it costs a new version. The internal
122 +setup and release runbook live with the server (private `sigit-si` repo).
123
124 ## Development
125
server-si-cli.json
server-sigit.json new
+39
@@ -0,0 +1,39 @@
1 +{
2 + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3 + "name": "si.sigit/sigit",
4 + "title": "siGit",
5 + "description": "MCP server for siGit (sigit.si): browse repos, search code, manage PRs/issues, web search.",
6 + "version": "1.2.5",
7 + "websiteUrl": "https://sigit.si",
8 + "repository": {
9 + "url": "https://github.com/getsigit/si",
10 + "source": "github"
11 + },
12 + "remotes": [
13 + {
14 + "type": "streamable-http",
15 + "url": "https://sigit.si/api/v1/mcp"
16 + }
17 + ],
18 + "_meta": {
19 + "io.modelcontextprotocol.registry/publisher-provided": {
20 + "categories": ["developer-tools", "version-control", "ai-agents"],
21 + "keywords": [
22 + "git",
23 + "git hosting",
24 + "mcp server",
25 + "model context protocol",
26 + "code search",
27 + "pull requests",
28 + "issues",
29 + "repositories",
30 + "web search",
31 + "coding agent",
32 + "ai coding agent",
33 + "ai agent tools",
34 + "developer tools",
35 + "software development"
36 + ]
37 + }
38 + }
39 +}
server.json
+50 -9
@@ -1,18 +1,59 @@
1 {
2 "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3 - "name": "si.sigit/sigit",
4 - "title": "siGit",
5 - "description": "MCP server for siGit (sigit.si): browse repos, search code, manage PRs/issues, web search.",
6 - "version": "1.2.5",
7 - "websiteUrl": "https://sigit.si",
3 + "name": "si.sigit/cli",
4 + "title": "siGit CLI",
5 + "description": "Run sigit.si locally: browse repos, search code, manage PRs, issues, and Cloud Sessions.",
6 + "version": "1.1.0",
7 + "websiteUrl": "https://github.com/getsigit/si",
8 "repository": {
9 "url": "https://github.com/getsigit/si",
10 "source": "github"
11 },
12 - "remotes": [
12 + "packages": [
13 {
14 - "type": "streamable-http",
15 - "url": "https://sigit.si/api/v1/mcp"
14 + "registryType": "cargo",
15 + "registryBaseUrl": "https://crates.io",
16 + "identifier": "sigit-si-cli",
17 + "version": "1.1.0",
18 + "transport": {
19 + "type": "stdio"
20 + },
21 + "packageArguments": [
22 + {
23 + "type": "positional",
24 + "value": "mcp",
25 + "valueHint": "mcp",
26 + "description": "Serve the CLI's commands as MCP tools over stdio instead of running one of them.",
27 + "isRequired": true
28 + },
29 + {
30 + "type": "named",
31 + "name": "--read-only",
32 + "description": "Drop every tool that writes, leaving only the ones that read.",
33 + "isRequired": false
34 + },
35 + {
36 + "type": "named",
37 + "name": "--toolsets",
38 + "description": "Comma-separated groups to expose, out of account, repo, issue, pr, code, hook, session, and billing. Defaults to all of them.",
39 + "isRequired": false
40 + }
41 + ],
42 + "environmentVariables": [
43 + {
44 + "name": "SIGIT_TOKEN",
45 + "description": "sigit.si session token. Optional: the server otherwise reuses whatever `si auth login` stored.",
46 + "isRequired": false,
47 + "isSecret": true,
48 + "format": "string"
49 + },
50 + {
51 + "name": "SIGIT_TOOLSETS",
52 + "description": "The environment form of --toolsets, for clients that pass configuration as variables rather than arguments.",
53 + "isRequired": false,
54 + "format": "string"
55 + }
56 + ]
57 }
58 ],
59 "_meta": {
@@ -21,13 +62,13 @@
62 "keywords": [
63 "git",
64 "git hosting",
65 + "cli",
66 "mcp server",
67 "model context protocol",
68 "code search",
69 "pull requests",
70 "issues",
71 "repositories",
30 - "web search",
72 "coding agent",
73 "ai coding agent",
74 "ai agent tools",