Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions .github/workflows/publish-mcp-server.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: Validate and publish FunASR MCP server

on:
pull_request:
paths:
- "examples/mcp_server/**"
- ".github/workflows/publish-mcp-server.yml"
push:
branches: ["main"]
tags: ["mcp-v*"]
paths:
- "examples/mcp_server/**"
- ".github/workflows/publish-mcp-server.yml"
workflow_dispatch:

env:
IMAGE_NAME: ghcr.io/modelscope/funasr-mcp
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf

jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v6
with:
python-version: "3.10"

- name: Run MCP unit and metadata tests
run: python -m unittest discover -s examples/mcp_server -p "test_*.py" -v

- name: Validate server.json against the official schema
run: |
python -m pip install --disable-pip-version-check "jsonschema==4.25.1"
schema_url="$(jq -r '."$schema"' examples/mcp_server/server.json)"
curl --fail --show-error --silent --location --retry 3 \
"$schema_url" -o "$RUNNER_TEMP/server.schema.json"
python - <<'PY'
import json
import os
from pathlib import Path

from jsonschema import Draft7Validator

metadata = json.loads(Path("examples/mcp_server/server.json").read_text())
schema = json.loads(
(Path(os.environ["RUNNER_TEMP"]) / "server.schema.json").read_text()
)
Draft7Validator(schema).validate(metadata)
PY

- name: Install verified MCP publisher
run: |
archive="$RUNNER_TEMP/mcp-publisher.tar.gz"
curl --fail --show-error --silent --location --retry 3 \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \
-o "$archive"
echo "${MCP_PUBLISHER_SHA256} $archive" | sha256sum --check
tar -xzf "$archive" -C "$RUNNER_TEMP" mcp-publisher

- name: Validate metadata with the official Registry
run: |
"$RUNNER_TEMP/mcp-publisher" validate examples/mcp_server/server.json

- name: Check release tag matches server version
id: metadata
shell: bash
run: |
version="$(jq -r '.version' examples/mcp_server/server.json)"
echo "version=$version" >> "$GITHUB_OUTPUT"
if [[ "$GITHUB_REF" == refs/tags/mcp-v* ]]; then
tag_version="${GITHUB_REF_NAME#mcp-v}"
test "$tag_version" = "$version" || {
echo "Tag version $tag_version does not match server.json $version" >&2
exit 1
}
fi

- uses: docker/setup-buildx-action@v4

- name: Build local MCP image
uses: docker/build-push-action@v7
with:
context: examples/mcp_server
load: true
push: false
tags: funasr-mcp:test
build-args: |
VERSION=${{ steps.metadata.outputs.version }}
VCS_REF=${{ github.sha }}
cache-from: type=gha,scope=funasr-mcp
cache-to: type=gha,mode=max,scope=funasr-mcp

- name: Smoke-test MCP protocol inside the image
run: python examples/mcp_server/smoke_test.py funasr-mcp:test

publish:
if: startsWith(github.ref, 'refs/tags/mcp-v')
needs: validate
runs-on: ubuntu-latest
environment: mcp-registry-publish
permissions:
contents: read
id-token: write
packages: write

steps:
- uses: actions/checkout@v7

- name: Read server version
id: metadata
run: echo "version=$(jq -r '.version' examples/mcp_server/server.json)" >> "$GITHUB_OUTPUT"

- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/setup-buildx-action@v4

- name: Publish versioned MCP image
uses: docker/build-push-action@v7
with:
context: examples/mcp_server
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.metadata.outputs.version }}
${{ env.IMAGE_NAME }}:latest
build-args: |
VERSION=${{ steps.metadata.outputs.version }}
VCS_REF=${{ github.sha }}
cache-from: type=gha,scope=funasr-mcp
cache-to: type=gha,mode=max,scope=funasr-mcp

- name: Make the GHCR package public
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api --method PATCH \
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/funasr-mcp" \
-f visibility=public

- name: Install verified MCP publisher
run: |
archive="$RUNNER_TEMP/mcp-publisher.tar.gz"
curl --fail --show-error --silent --location --retry 3 \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \
-o "$archive"
echo "${MCP_PUBLISHER_SHA256} $archive" | sha256sum --check
tar -xzf "$archive" -C "$RUNNER_TEMP" mcp-publisher

- name: Publish metadata to the official MCP Registry
working-directory: examples/mcp_server
run: |
"$RUNNER_TEMP/mcp-publisher" login github-oidc
"$RUNNER_TEMP/mcp-publisher" publish

- name: Verify the Registry listing
run: |
for attempt in {1..6}; do
if curl --fail --show-error --silent --get \
--data-urlencode "search=io.github.modelscope/funasr-mcp" \
https://registry.modelcontextprotocol.io/v0.1/servers \
| jq -e '.servers[] | select(.server.name == "io.github.modelscope/funasr-mcp")' \
>/dev/null; then
exit 0
fi
sleep 10
done
echo "Published server did not appear in the Registry within 60 seconds" >&2
exit 1
15 changes: 12 additions & 3 deletions examples/mcp_server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
FROM python:3.10-slim

LABEL io.modelcontextprotocol.server.name="io.github.modelscope/funasr-mcp"
ARG FUNASR_VERSION=1.3.14
ARG VERSION=0.1.0
ARG VCS_REF=unknown

LABEL io.modelcontextprotocol.server.name="io.github.modelscope/funasr-mcp" \
org.opencontainers.image.title="FunASR MCP Server" \
org.opencontainers.image.description="Local speech transcription over the Model Context Protocol" \
org.opencontainers.image.source="https://github.com/modelscope/FunASR" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.licenses="MIT"

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
Expand All @@ -15,8 +25,7 @@ RUN apt-get update \
libsndfile1 \
&& rm -rf /var/lib/apt/lists/*

RUN python -m pip install --upgrade pip \
&& pip install funasr
RUN pip install "funasr==${FUNASR_VERSION}"

COPY funasr_mcp.py /app/funasr_mcp.py

Expand Down
49 changes: 41 additions & 8 deletions examples/mcp_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,62 @@ checks that initialize the server and call `tools/list`.
docker build -t funasr-mcp examples/mcp_server
docker run --rm -i \
-e FUNASR_DEVICE=cpu \
-v /path/to/audio:/audio:ro \
--mount type=bind,src=/path/to/audio,dst=/audio,readonly \
--mount type=volume,src=funasr-mcp-cache,dst=/root/.cache/modelscope \
funasr-mcp
```

Verify the image entrypoint and MCP handshake without downloading a model:

```bash
python examples/mcp_server/smoke_test.py funasr-mcp
```

When submitting this server to MCP directories such as Glama, use this folder as
the Docker build context so the container entrypoint runs `funasr_mcp.py`.
The repository root `glama.json` declares GitHub maintainer ownership for Glama,
while the `glama.json` file in this directory declares the container command and
metadata for directory scanners.

### Official MCP Registry checklist
### Official MCP Registry

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

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

Before publishing, push a public OCI image (for example to GHCR) and create a
matching `server.json` whose `name` is `io.github.modelscope/funasr-mcp` and
whose package identifier points at that image tag. The Registry verifies that
the Docker/OCI label and `server.json` name match.
The release workflow validates the metadata against the official schema, builds
the image, performs an MCP `initialize` and `tools/list` handshake, pushes the
versioned image to GHCR, and publishes `server.json` through the official
`mcp-publisher` CLI.

To release a new MCP server version:

1. Update `version` and the OCI image tag in `server.json` together.
2. Merge the change after the MCP validation workflow passes.
3. Have a `modelscope` organization Owner push the matching `mcp-v<version>`
tag, for example `mcp-v0.1.0`.
4. Approve the protected `mcp-registry-publish` environment deployment.

The official Registry only grants the `io.github.modelscope/*` namespace to a
GitHub organization Owner. Keep the publish environment restricted to release
tags and require a maintainer approval because its OIDC token can publish the
organization namespace.

After publication, clients can run the pinned image directly:

```bash
docker run --rm -i \
--mount type=bind,src=/path/to/audio,dst=/audio,readonly \
--mount type=volume,src=funasr-mcp-cache,dst=/root/.cache/modelscope \
ghcr.io/modelscope/funasr-mcp:0.1.0
```

When using the container, pass tool paths under `/audio`, such as
`/audio/meeting.wav`.

### Glama submission checklist

Expand Down
46 changes: 46 additions & 0 deletions examples/mcp_server/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.modelscope/funasr-mcp",
"title": "FunASR",
"description": "Transcribe local audio with FunASR and SenseVoice using private, on-device inference.",
"version": "0.1.0",
"websiteUrl": "https://github.com/modelscope/FunASR/tree/main/examples/mcp_server",
"repository": {
"url": "https://github.com/modelscope/FunASR",
"source": "github",
"id": "569959091",
"subfolder": "examples/mcp_server"
},
"packages": [
{
"registryType": "oci",
"identifier": "ghcr.io/modelscope/funasr-mcp:0.1.0",
"runtimeHint": "docker",
"runtimeArguments": [
{
"type": "named",
"name": "--mount",
"description": "Mount the host audio directory read-only at /audio in the container.",
"value": "type=bind,src={audio_directory},dst=/audio,readonly",
"variables": {
"audio_directory": {
"description": "Host directory containing audio files. Use /audio/<file> when calling the tool.",
"format": "filepath",
"isRequired": true,
"placeholder": "/path/to/audio"
}
}
},
{
"type": "named",
"name": "--mount",
"description": "Persist downloaded ModelScope model files between container runs.",
"value": "type=volume,src=funasr-mcp-cache,dst=/root/.cache/modelscope"
}
],
"transport": {
"type": "stdio"
}
}
]
}
Loading
Loading