Skip to content

Commit ddd9a3f

Browse files
authored
Publish FunASR MCP server to the official Registry (#3228)
Add versioned OCI metadata, GHCR and OIDC release automation, strict container smoke tests, and release documentation.
1 parent 970eec7 commit ddd9a3f

6 files changed

Lines changed: 524 additions & 11 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
name: Validate and publish FunASR MCP server
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "examples/mcp_server/**"
7+
- ".github/workflows/publish-mcp-server.yml"
8+
push:
9+
branches: ["main"]
10+
tags: ["mcp-v*"]
11+
paths:
12+
- "examples/mcp_server/**"
13+
- ".github/workflows/publish-mcp-server.yml"
14+
workflow_dispatch:
15+
16+
env:
17+
IMAGE_NAME: ghcr.io/modelscope/funasr-mcp
18+
MCP_PUBLISHER_VERSION: v1.8.0
19+
MCP_PUBLISHER_SHA256: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
20+
21+
jobs:
22+
validate:
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
27+
steps:
28+
- uses: actions/checkout@v7
29+
30+
- uses: actions/setup-python@v6
31+
with:
32+
python-version: "3.10"
33+
34+
- name: Run MCP unit and metadata tests
35+
run: python -m unittest discover -s examples/mcp_server -p "test_*.py" -v
36+
37+
- name: Validate server.json against the official schema
38+
run: |
39+
python -m pip install --disable-pip-version-check "jsonschema==4.25.1"
40+
schema_url="$(jq -r '."$schema"' examples/mcp_server/server.json)"
41+
curl --fail --show-error --silent --location --retry 3 \
42+
"$schema_url" -o "$RUNNER_TEMP/server.schema.json"
43+
python - <<'PY'
44+
import json
45+
import os
46+
from pathlib import Path
47+
48+
from jsonschema import Draft7Validator
49+
50+
metadata = json.loads(Path("examples/mcp_server/server.json").read_text())
51+
schema = json.loads(
52+
(Path(os.environ["RUNNER_TEMP"]) / "server.schema.json").read_text()
53+
)
54+
Draft7Validator(schema).validate(metadata)
55+
PY
56+
57+
- name: Install verified MCP publisher
58+
run: |
59+
archive="$RUNNER_TEMP/mcp-publisher.tar.gz"
60+
curl --fail --show-error --silent --location --retry 3 \
61+
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \
62+
-o "$archive"
63+
echo "${MCP_PUBLISHER_SHA256} $archive" | sha256sum --check
64+
tar -xzf "$archive" -C "$RUNNER_TEMP" mcp-publisher
65+
66+
- name: Validate metadata with the official Registry
67+
run: |
68+
"$RUNNER_TEMP/mcp-publisher" validate examples/mcp_server/server.json
69+
70+
- name: Check release tag matches server version
71+
id: metadata
72+
shell: bash
73+
run: |
74+
version="$(jq -r '.version' examples/mcp_server/server.json)"
75+
echo "version=$version" >> "$GITHUB_OUTPUT"
76+
if [[ "$GITHUB_REF" == refs/tags/mcp-v* ]]; then
77+
tag_version="${GITHUB_REF_NAME#mcp-v}"
78+
test "$tag_version" = "$version" || {
79+
echo "Tag version $tag_version does not match server.json $version" >&2
80+
exit 1
81+
}
82+
fi
83+
84+
- uses: docker/setup-buildx-action@v4
85+
86+
- name: Build local MCP image
87+
uses: docker/build-push-action@v7
88+
with:
89+
context: examples/mcp_server
90+
load: true
91+
push: false
92+
tags: funasr-mcp:test
93+
build-args: |
94+
VERSION=${{ steps.metadata.outputs.version }}
95+
VCS_REF=${{ github.sha }}
96+
cache-from: type=gha,scope=funasr-mcp
97+
cache-to: type=gha,mode=max,scope=funasr-mcp
98+
99+
- name: Smoke-test MCP protocol inside the image
100+
run: python examples/mcp_server/smoke_test.py funasr-mcp:test
101+
102+
publish:
103+
if: startsWith(github.ref, 'refs/tags/mcp-v')
104+
needs: validate
105+
runs-on: ubuntu-latest
106+
environment: mcp-registry-publish
107+
permissions:
108+
contents: read
109+
id-token: write
110+
packages: write
111+
112+
steps:
113+
- uses: actions/checkout@v7
114+
115+
- name: Read server version
116+
id: metadata
117+
run: echo "version=$(jq -r '.version' examples/mcp_server/server.json)" >> "$GITHUB_OUTPUT"
118+
119+
- uses: docker/login-action@v4
120+
with:
121+
registry: ghcr.io
122+
username: ${{ github.actor }}
123+
password: ${{ secrets.GITHUB_TOKEN }}
124+
125+
- uses: docker/setup-buildx-action@v4
126+
127+
- name: Publish versioned MCP image
128+
uses: docker/build-push-action@v7
129+
with:
130+
context: examples/mcp_server
131+
push: true
132+
tags: |
133+
${{ env.IMAGE_NAME }}:${{ steps.metadata.outputs.version }}
134+
${{ env.IMAGE_NAME }}:latest
135+
build-args: |
136+
VERSION=${{ steps.metadata.outputs.version }}
137+
VCS_REF=${{ github.sha }}
138+
cache-from: type=gha,scope=funasr-mcp
139+
cache-to: type=gha,mode=max,scope=funasr-mcp
140+
141+
- name: Make the GHCR package public
142+
env:
143+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
144+
run: |
145+
gh api --method PATCH \
146+
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/funasr-mcp" \
147+
-f visibility=public
148+
149+
- name: Install verified MCP publisher
150+
run: |
151+
archive="$RUNNER_TEMP/mcp-publisher.tar.gz"
152+
curl --fail --show-error --silent --location --retry 3 \
153+
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \
154+
-o "$archive"
155+
echo "${MCP_PUBLISHER_SHA256} $archive" | sha256sum --check
156+
tar -xzf "$archive" -C "$RUNNER_TEMP" mcp-publisher
157+
158+
- name: Publish metadata to the official MCP Registry
159+
working-directory: examples/mcp_server
160+
run: |
161+
"$RUNNER_TEMP/mcp-publisher" login github-oidc
162+
"$RUNNER_TEMP/mcp-publisher" publish
163+
164+
- name: Verify the Registry listing
165+
run: |
166+
for attempt in {1..6}; do
167+
if curl --fail --show-error --silent --get \
168+
--data-urlencode "search=io.github.modelscope/funasr-mcp" \
169+
https://registry.modelcontextprotocol.io/v0.1/servers \
170+
| jq -e '.servers[] | select(.server.name == "io.github.modelscope/funasr-mcp")' \
171+
>/dev/null; then
172+
exit 0
173+
fi
174+
sleep 10
175+
done
176+
echo "Published server did not appear in the Registry within 60 seconds" >&2
177+
exit 1

examples/mcp_server/Dockerfile

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
FROM python:3.10-slim
22

3-
LABEL io.modelcontextprotocol.server.name="io.github.modelscope/funasr-mcp"
3+
ARG FUNASR_VERSION=1.3.14
4+
ARG VERSION=0.1.0
5+
ARG VCS_REF=unknown
6+
7+
LABEL io.modelcontextprotocol.server.name="io.github.modelscope/funasr-mcp" \
8+
org.opencontainers.image.title="FunASR MCP Server" \
9+
org.opencontainers.image.description="Local speech transcription over the Model Context Protocol" \
10+
org.opencontainers.image.source="https://github.com/modelscope/FunASR" \
11+
org.opencontainers.image.version="${VERSION}" \
12+
org.opencontainers.image.revision="${VCS_REF}" \
13+
org.opencontainers.image.licenses="MIT"
414

515
ENV PYTHONDONTWRITEBYTECODE=1 \
616
PYTHONUNBUFFERED=1 \
@@ -15,8 +25,7 @@ RUN apt-get update \
1525
libsndfile1 \
1626
&& rm -rf /var/lib/apt/lists/*
1727

18-
RUN python -m pip install --upgrade pip \
19-
&& pip install funasr
28+
RUN pip install "funasr==${FUNASR_VERSION}"
2029

2130
COPY funasr_mcp.py /app/funasr_mcp.py
2231

examples/mcp_server/README.md

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,62 @@ checks that initialize the server and call `tools/list`.
1919
docker build -t funasr-mcp examples/mcp_server
2020
docker run --rm -i \
2121
-e FUNASR_DEVICE=cpu \
22-
-v /path/to/audio:/audio:ro \
22+
--mount type=bind,src=/path/to/audio,dst=/audio,readonly \
23+
--mount type=volume,src=funasr-mcp-cache,dst=/root/.cache/modelscope \
2324
funasr-mcp
2425
```
2526

27+
Verify the image entrypoint and MCP handshake without downloading a model:
28+
29+
```bash
30+
python examples/mcp_server/smoke_test.py funasr-mcp
31+
```
32+
2633
When submitting this server to MCP directories such as Glama, use this folder as
2734
the Docker build context so the container entrypoint runs `funasr_mcp.py`.
2835
The repository root `glama.json` declares GitHub maintainer ownership for Glama,
2936
while the `glama.json` file in this directory declares the container command and
3037
metadata for directory scanners.
3138

32-
### Official MCP Registry checklist
39+
### Official MCP Registry
3340

34-
The Dockerfile includes the OCI ownership label expected by the official MCP
35-
Registry:
41+
The versioned Registry metadata is in [`server.json`](server.json). It points to
42+
the public GHCR image and asks clients to mount one host audio directory at
43+
`/audio` read-only. The Dockerfile carries the matching OCI ownership label:
3644

3745
```dockerfile
3846
LABEL io.modelcontextprotocol.server.name="io.github.modelscope/funasr-mcp"
3947
```
4048

41-
Before publishing, push a public OCI image (for example to GHCR) and create a
42-
matching `server.json` whose `name` is `io.github.modelscope/funasr-mcp` and
43-
whose package identifier points at that image tag. The Registry verifies that
44-
the Docker/OCI label and `server.json` name match.
49+
The release workflow validates the metadata against the official schema, builds
50+
the image, performs an MCP `initialize` and `tools/list` handshake, pushes the
51+
versioned image to GHCR, and publishes `server.json` through the official
52+
`mcp-publisher` CLI.
53+
54+
To release a new MCP server version:
55+
56+
1. Update `version` and the OCI image tag in `server.json` together.
57+
2. Merge the change after the MCP validation workflow passes.
58+
3. Have a `modelscope` organization Owner push the matching `mcp-v<version>`
59+
tag, for example `mcp-v0.1.0`.
60+
4. Approve the protected `mcp-registry-publish` environment deployment.
61+
62+
The official Registry only grants the `io.github.modelscope/*` namespace to a
63+
GitHub organization Owner. Keep the publish environment restricted to release
64+
tags and require a maintainer approval because its OIDC token can publish the
65+
organization namespace.
66+
67+
After publication, clients can run the pinned image directly:
68+
69+
```bash
70+
docker run --rm -i \
71+
--mount type=bind,src=/path/to/audio,dst=/audio,readonly \
72+
--mount type=volume,src=funasr-mcp-cache,dst=/root/.cache/modelscope \
73+
ghcr.io/modelscope/funasr-mcp:0.1.0
74+
```
75+
76+
When using the container, pass tool paths under `/audio`, such as
77+
`/audio/meeting.wav`.
4578

4679
### Glama submission checklist
4780

examples/mcp_server/server.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3+
"name": "io.github.modelscope/funasr-mcp",
4+
"title": "FunASR",
5+
"description": "Transcribe local audio with FunASR and SenseVoice using private, on-device inference.",
6+
"version": "0.1.0",
7+
"websiteUrl": "https://github.com/modelscope/FunASR/tree/main/examples/mcp_server",
8+
"repository": {
9+
"url": "https://github.com/modelscope/FunASR",
10+
"source": "github",
11+
"id": "569959091",
12+
"subfolder": "examples/mcp_server"
13+
},
14+
"packages": [
15+
{
16+
"registryType": "oci",
17+
"identifier": "ghcr.io/modelscope/funasr-mcp:0.1.0",
18+
"runtimeHint": "docker",
19+
"runtimeArguments": [
20+
{
21+
"type": "named",
22+
"name": "--mount",
23+
"description": "Mount the host audio directory read-only at /audio in the container.",
24+
"value": "type=bind,src={audio_directory},dst=/audio,readonly",
25+
"variables": {
26+
"audio_directory": {
27+
"description": "Host directory containing audio files. Use /audio/<file> when calling the tool.",
28+
"format": "filepath",
29+
"isRequired": true,
30+
"placeholder": "/path/to/audio"
31+
}
32+
}
33+
},
34+
{
35+
"type": "named",
36+
"name": "--mount",
37+
"description": "Persist downloaded ModelScope model files between container runs.",
38+
"value": "type=volume,src=funasr-mcp-cache,dst=/root/.cache/modelscope"
39+
}
40+
],
41+
"transport": {
42+
"type": "stdio"
43+
}
44+
}
45+
]
46+
}

0 commit comments

Comments
 (0)