chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:10 +08:00
commit c397331b1e
3684 changed files with 990993 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.10-slim
LABEL io.modelcontextprotocol.server.name="io.github.modelscope/funasr-mcp"
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
FUNASR_DEVICE=cpu
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
libsndfile1 \
&& rm -rf /var/lib/apt/lists/*
RUN python -m pip install --upgrade pip \
&& pip install funasr
COPY funasr_mcp.py /app/funasr_mcp.py
CMD ["python", "/app/funasr_mcp.py"]
+152
View File
@@ -0,0 +1,152 @@
# FunASR MCP Server
[Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI assistants the ability to transcribe audio.
## Setup
### 1. Install dependencies
```bash
pip install funasr
```
### 2. Optional: run with Docker
The Dockerfile starts the MCP server over stdio and is suitable for MCP directory
checks that initialize the server and call `tools/list`.
```bash
docker build -t funasr-mcp examples/mcp_server
docker run --rm -i \
-e FUNASR_DEVICE=cpu \
-v /path/to/audio:/audio:ro \
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
The Dockerfile includes the OCI ownership label expected by the official MCP
Registry:
```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.
### Glama submission checklist
Use these values when adding the server at <https://glama.ai/mcp/servers>:
| Field | Value |
|------|-------|
| Repository URL | <https://github.com/modelscope/FunASR> |
| Docker build context | `examples/mcp_server` |
| Dockerfile path | `examples/mcp_server/Dockerfile` |
| Server command | `python /app/funasr_mcp.py` |
| Expected MCP tool | `transcribe_audio` |
After Glama finishes evaluation, verify that the score badge endpoint returns
success before adding it to directory PRs:
```markdown
[![modelscope/FunASR MCP server](https://glama.ai/mcp/servers/modelscope/FunASR/badges/score.svg)](https://glama.ai/mcp/servers/modelscope/FunASR)
```
If the badge endpoint still returns 404, keep the badge out of external
directory submissions until the Glama listing is live.
### Directory listings
The FunASR MCP server is listed on mcp.so:
- <https://mcp.so/server/mcp-server-funasr/radial-hks>
### 3. Configure your AI tool
**Claude Code** (`~/.claude.json`):
```json
{
"mcpServers": {
"funasr": {
"command": "python",
"args": ["/path/to/examples/mcp_server/funasr_mcp.py"],
"env": {"FUNASR_DEVICE": "cuda"}
}
}
}
```
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"funasr": {
"command": "python",
"args": ["/path/to/funasr_mcp.py"],
"env": {"FUNASR_DEVICE": "cpu"}
}
}
}
```
**Cursor** (Settings → MCP Servers → Add):
- Command: `python /path/to/funasr_mcp.py`
- Environment: `FUNASR_DEVICE=cuda`
## Tools
### `transcribe_audio`
Transcribe a speech audio file to text.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `audio_path` | string | Yes | Path to audio file (wav, mp3, flac, m4a, ogg) |
| `language` | string | No | Language hint (auto-detected by default) |
**Returns:** Transcribed text with timestamps and speaker labels (when available).
## Example Usage
Once configured, ask your AI assistant:
- "Transcribe the meeting recording at ~/Downloads/meeting.wav"
- "What was said in this audio file? /path/to/interview.mp3"
- "Convert this voice memo to text: ~/voice_note.m4a"
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FUNASR_DEVICE` | `cpu` | Device: `cuda`, `cpu`, or `mps` |
| `FUNASR_MODEL` | `iic/SenseVoiceSmall` | ASR model to use |
## Features
- **50+ languages** with automatic detection
- **Speaker diarization** — identifies who said what
- **Timestamps** — per-segment timing
- **170x realtime on GPU**, 17x on CPU
- **No API key needed** — fully local inference
- MIT licensed, privacy-friendly (audio never leaves your machine)
## Verified Compatibility
| Tool | Status |
|------|--------|
| Claude Code | ✅ Tested |
| Claude Desktop | ✅ Compatible |
| Cursor | ✅ Compatible |
| Windsurf | ✅ Compatible |
| Any MCP client | ✅ Standard protocol |
+179
View File
@@ -0,0 +1,179 @@
"""
FunASR MCP Server
Model Context Protocol server that exposes FunASR speech recognition
as a tool for AI assistants (Claude, Cursor, etc).
Usage:
python funasr_mcp.py
Add to claude_desktop_config.json:
{
"mcpServers": {
"funasr": {
"command": "python",
"args": ["path/to/funasr_mcp.py"]
}
}
}
"""
import json
import sys
import os
import tempfile
import base64
# MCP protocol over stdio
def send_response(id, result):
msg = {"jsonrpc": "2.0", "id": id, "result": result}
out = json.dumps(msg)
sys.stdout.write(f"Content-Length: {len(out)}\r\n\r\n{out}")
sys.stdout.flush()
def send_notification(method, params=None):
msg = {"jsonrpc": "2.0", "method": method, "params": params or {}}
out = json.dumps(msg)
sys.stdout.write(f"Content-Length: {len(out)}\r\n\r\n{out}")
sys.stdout.flush()
_model = None
def get_model():
global _model
if _model is None:
from funasr import AutoModel
device = os.environ.get("FUNASR_DEVICE", "cpu")
_model = AutoModel(
model="iic/SenseVoiceSmall",
vad_model="fsmn-vad",
vad_kwargs={"max_single_segment_time": 30000},
device=device,
disable_update=True,
)
return _model
def transcribe(audio_path: str, language: str = "auto") -> dict:
"""Transcribe an audio file to text."""
import re
model = get_model()
result = model.generate(input=audio_path, batch_size=1)
text = result[0]["text"]
text = re.sub(r'<\|[^|]*\|>', '', text).strip()
response = {"text": text}
if "sentence_info" in result[0]:
response["segments"] = [
{
"text": seg.get("text", ""),
"start": seg.get("start", 0) / 1000.0,
"end": seg.get("end", 0) / 1000.0,
"speaker": seg.get("spk", None),
}
for seg in result[0]["sentence_info"]
]
return response
def handle_request(request):
method = request.get("method")
id = request.get("id")
params = request.get("params", {})
if method == "initialize":
send_response(id, {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {"listChanged": False}},
"serverInfo": {"name": "funasr", "version": "1.3.2"},
})
elif method == "tools/list":
send_response(id, {
"tools": [
{
"name": "transcribe_audio",
"description": "Transcribe speech audio to text. Supports 50+ languages, auto-detection, speaker diarization. Input: file path to audio.",
"inputSchema": {
"type": "object",
"properties": {
"audio_path": {
"type": "string",
"description": "Path to audio file (wav, mp3, flac, etc)"
},
"language": {
"type": "string",
"description": "Language hint (optional, auto-detected by default)",
"default": "auto"
}
},
"required": ["audio_path"]
}
}
]
})
elif method == "tools/call":
tool_name = params.get("name")
args = params.get("arguments", {})
if tool_name == "transcribe_audio":
audio_path = args.get("audio_path", "")
language = args.get("language", "auto")
if not os.path.exists(audio_path):
send_response(id, {
"content": [{"type": "text", "text": f"Error: file not found: {audio_path}"}],
"isError": True
})
return
result = transcribe(audio_path, language)
text_output = f"Transcription: {result['text']}"
if "segments" in result:
text_output += "\n\nSegments:"
for seg in result["segments"]:
spk = f" [Speaker {seg['speaker']}]" if seg.get('speaker') is not None else ""
text_output += f"\n [{seg['start']:.1f}s - {seg['end']:.1f}s]{spk} {seg['text']}"
send_response(id, {
"content": [{"type": "text", "text": text_output}]
})
else:
send_response(id, {
"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}],
"isError": True
})
elif method == "notifications/initialized":
pass # Client confirmed initialization
else:
if id is not None:
send_response(id, {})
def main():
"""Run MCP server over stdio."""
import re
buffer = ""
while True:
line = sys.stdin.readline()
if not line:
break
buffer += line
if "\r\n\r\n" in buffer:
header, body_start = buffer.split("\r\n\r\n", 1)
match = re.search(r"Content-Length: (\d+)", header)
if match:
length = int(match.group(1))
while len(body_start) < length:
body_start += sys.stdin.read(length - len(body_start))
request = json.loads(body_start[:length])
buffer = body_start[length:]
handle_request(request)
else:
buffer = ""
if __name__ == "__main__":
main()
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://glama.ai/mcp/schemas/server.json",
"maintainers": ["LauraGPT"],
"name": "funasr",
"display_name": "FunASR MCP Server",
"description": "Local speech recognition MCP server powered by FunASR and SenseVoice. It exposes a transcribe_audio tool for privacy-friendly audio transcription from local files.",
"repository": "https://github.com/modelscope/FunASR",
"license": "MIT",
"runtime": "python",
"tags": ["speech-to-text", "asr", "audio", "funasr", "sensevoice", "local", "mcp"],
"mcpServers": {
"funasr": {
"command": "python",
"args": ["/app/funasr_mcp.py"],
"env": {
"FUNASR_DEVICE": "cpu"
}
}
}
}