chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"]
|
||||
@@ -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
|
||||
[](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 |
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user