macwhisper-mcp-server 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: macwhisper-mcp-server
3
+ Version: 1.0.0
4
+ Summary: Local MCP server exposing MacWhisper transcription to Claude Desktop.
5
+ Author: Thomas
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/docdyhr/macwhisper-mcp-server
8
+ Project-URL: Issues, https://github.com/docdyhr/macwhisper-mcp-server/issues
9
+ Keywords: mcp,macwhisper,transcription,claude,llm
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: MacOS X
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
17
+ Requires-Python: <3.14,>=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: fastmcp==3.2.4
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+ Requires-Dist: pytest-mock>=3.12; extra == "dev"
24
+ Requires-Dist: ruff>=0.6; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # macwhisper-mcp-server
28
+
29
+ Local MCP server that connects [MacWhisper](https://goodsnooze.gumroad.com/l/macwhisper) to [Claude Desktop](https://claude.ai/download).
30
+
31
+ **What it does:** Drop an audio file on your Desktop, then ask Claude to transcribe it, summarise it, or pull out action items — in one step. MacWhisper does the transcription on your Mac; Claude does the thinking. Nothing leaves your machine. No cloud APIs. No data ever leaves your Mac.
32
+
33
+ ```
34
+ Audio file → MacWhisper CLI → MCP server → Claude Desktop
35
+ ```
36
+
37
+ [![CI](https://github.com/docdyhr/macwhisper-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/docdyhr/macwhisper-mcp-server/actions/workflows/ci.yml)
38
+
39
+ ---
40
+
41
+ ![Claude Desktop transcribing an audio file](images/MacWhisper-MCP-server.png)
42
+
43
+ ---
44
+
45
+ ## Requirements
46
+
47
+ - macOS (MacWhisper is macOS-only)
48
+ - [MacWhisper](https://goodsnooze.gumroad.com/l/macwhisper) — installed, licensed, CLI enabled in Settings
49
+ - Python 3.13.x via [pyenv](https://github.com/pyenv/pyenv)
50
+ - [Claude Desktop](https://claude.ai/download)
51
+
52
+ ---
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ git clone https://github.com/docdyhr/macwhisper-mcp-server.git
58
+ cd macwhisper-mcp-server
59
+
60
+ pyenv install 3.13.13 # skip if already installed
61
+ pyenv local 3.13.13
62
+ python -m venv .venv
63
+ source .venv/bin/activate
64
+ pip install -e .
65
+ ```
66
+
67
+ Verify the MacWhisper CLI is reachable:
68
+
69
+ ```bash
70
+ /Applications/MacWhisper.app/Contents/MacOS/mw --help
71
+ ```
72
+
73
+ If you get "command not found": open MacWhisper → Settings → enable CLI.
74
+
75
+ ---
76
+
77
+ ## Configure Claude Desktop
78
+
79
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` and add:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "macwhisper": {
85
+ "command": "/Users/<you>/macwhisper-mcp-server/.venv/bin/macwhisper-mcp",
86
+ "args": [],
87
+ "env": {
88
+ "MACWHISPER_ALLOWED_PATHS": "/Users/<you>/Desktop:/Users/<you>/Downloads"
89
+ }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ Replace `<you>` with your macOS username. Restart Claude Desktop.
96
+
97
+ ### Verify it works
98
+
99
+ In Claude Desktop, ask:
100
+
101
+ > Transcribe ~/Desktop/memo.m4a
102
+
103
+ You should see a `transcribe_audio` tool call appear, followed by the transcript.
104
+
105
+ ---
106
+
107
+ ## Available tools
108
+
109
+ | Tool | Description |
110
+ |------|-------------|
111
+ | `transcribe_audio(path, model?)` | Transcribe an audio file and return the transcript as plain text |
112
+ | `cancel_transcription()` | Cancel the currently running transcription |
113
+ | `list_allowed_paths()` | Return the directories the server is allowed to read from |
114
+ | `start_watch(folder)` | Watch a folder and auto-transcribe new audio files into `../done/` |
115
+ | `stop_watch()` | Stop the active folder watcher |
116
+ | `get_watch_results()` | Return completed watch-folder transcriptions and clear the queue |
117
+
118
+ Supported audio formats: `.m4a` `.mp3` `.mp4` `.mov` `.wav` `.aiff` `.flac`
119
+
120
+ ---
121
+
122
+ ## Configuration
123
+
124
+ All configuration is via environment variables. Pass them through the `env` dict in `claude_desktop_config.json` (for Claude Desktop) or set them in `.env` for local development.
125
+
126
+ | Env var | Default | Description |
127
+ |---------|---------|-------------|
128
+ | `MACWHISPER_ALLOWED_PATHS` | `~/Desktop` | Colon-separated list of directories the server may read from |
129
+ | `MACWHISPER_CLI` | auto-detected | Path to the `mw` binary. Defaults to `/Applications/MacWhisper.app/Contents/MacOS/mw` if that file exists, otherwise `mw` on `PATH` |
130
+ | `MACWHISPER_LOG_PATH` | `~/Library/Logs/macwhisper-mcp.log` | Log file path (never stdout — that's reserved for MCP) |
131
+
132
+ **Local development:** copy `.env.example` to `.env` and adjust. With [direnv](https://direnv.net/), `.envrc` exports `.env` automatically. Without direnv: `source .env`.
133
+
134
+ ---
135
+
136
+ ## Development
137
+
138
+ ```bash
139
+ source .venv/bin/activate
140
+ pip install -e ".[dev]"
141
+
142
+ # Tests
143
+ pytest -q
144
+
145
+ # Lint + format
146
+ ruff check .
147
+ ruff format .
148
+
149
+ # Pre-commit hooks (one-time setup)
150
+ pip install pre-commit
151
+ pre-commit install
152
+
153
+ # Smoke-test against a real audio file (server must not be running in Claude Desktop)
154
+ python scripts/smoke_test.py ~/Downloads/Test.m4a
155
+ ```
156
+
157
+ ### Logs
158
+
159
+ ```bash
160
+ tail -f ~/Library/Logs/macwhisper-mcp.log
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Security
166
+
167
+ - All file paths are resolved (symlinks followed) and checked against the `MACWHISPER_ALLOWED_PATHS` allow-list before anything reaches the CLI.
168
+ - `subprocess.run` is always called with an argv list — never `shell=True`.
169
+ - No network calls. Ever.
170
+
171
+ See [PRD §7](./PRD.md) for the full threat model.
172
+
173
+ ---
174
+
175
+ ## Known limitations
176
+
177
+ - **Danish letter names:** Whisper may phonetically approximate letter names (e.g. "Æ, Ø, Å" → "E, Y, U") when they are spoken in isolation. Letters *inside words* transcribe correctly. This is a Whisper engine limitation, not a bug in this wrapper. See [PRD §12](./PRD.md).
178
+ - **Cold-start latency:** First transcription after MacWhisper launches takes ~13s (model load). Subsequent calls are ~2s.
179
+
180
+ ---
181
+
182
+ ## License
183
+
184
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,158 @@
1
+ # macwhisper-mcp-server
2
+
3
+ Local MCP server that connects [MacWhisper](https://goodsnooze.gumroad.com/l/macwhisper) to [Claude Desktop](https://claude.ai/download).
4
+
5
+ **What it does:** Drop an audio file on your Desktop, then ask Claude to transcribe it, summarise it, or pull out action items — in one step. MacWhisper does the transcription on your Mac; Claude does the thinking. Nothing leaves your machine. No cloud APIs. No data ever leaves your Mac.
6
+
7
+ ```
8
+ Audio file → MacWhisper CLI → MCP server → Claude Desktop
9
+ ```
10
+
11
+ [![CI](https://github.com/docdyhr/macwhisper-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/docdyhr/macwhisper-mcp-server/actions/workflows/ci.yml)
12
+
13
+ ---
14
+
15
+ ![Claude Desktop transcribing an audio file](images/MacWhisper-MCP-server.png)
16
+
17
+ ---
18
+
19
+ ## Requirements
20
+
21
+ - macOS (MacWhisper is macOS-only)
22
+ - [MacWhisper](https://goodsnooze.gumroad.com/l/macwhisper) — installed, licensed, CLI enabled in Settings
23
+ - Python 3.13.x via [pyenv](https://github.com/pyenv/pyenv)
24
+ - [Claude Desktop](https://claude.ai/download)
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ git clone https://github.com/docdyhr/macwhisper-mcp-server.git
32
+ cd macwhisper-mcp-server
33
+
34
+ pyenv install 3.13.13 # skip if already installed
35
+ pyenv local 3.13.13
36
+ python -m venv .venv
37
+ source .venv/bin/activate
38
+ pip install -e .
39
+ ```
40
+
41
+ Verify the MacWhisper CLI is reachable:
42
+
43
+ ```bash
44
+ /Applications/MacWhisper.app/Contents/MacOS/mw --help
45
+ ```
46
+
47
+ If you get "command not found": open MacWhisper → Settings → enable CLI.
48
+
49
+ ---
50
+
51
+ ## Configure Claude Desktop
52
+
53
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` and add:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "macwhisper": {
59
+ "command": "/Users/<you>/macwhisper-mcp-server/.venv/bin/macwhisper-mcp",
60
+ "args": [],
61
+ "env": {
62
+ "MACWHISPER_ALLOWED_PATHS": "/Users/<you>/Desktop:/Users/<you>/Downloads"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ Replace `<you>` with your macOS username. Restart Claude Desktop.
70
+
71
+ ### Verify it works
72
+
73
+ In Claude Desktop, ask:
74
+
75
+ > Transcribe ~/Desktop/memo.m4a
76
+
77
+ You should see a `transcribe_audio` tool call appear, followed by the transcript.
78
+
79
+ ---
80
+
81
+ ## Available tools
82
+
83
+ | Tool | Description |
84
+ |------|-------------|
85
+ | `transcribe_audio(path, model?)` | Transcribe an audio file and return the transcript as plain text |
86
+ | `cancel_transcription()` | Cancel the currently running transcription |
87
+ | `list_allowed_paths()` | Return the directories the server is allowed to read from |
88
+ | `start_watch(folder)` | Watch a folder and auto-transcribe new audio files into `../done/` |
89
+ | `stop_watch()` | Stop the active folder watcher |
90
+ | `get_watch_results()` | Return completed watch-folder transcriptions and clear the queue |
91
+
92
+ Supported audio formats: `.m4a` `.mp3` `.mp4` `.mov` `.wav` `.aiff` `.flac`
93
+
94
+ ---
95
+
96
+ ## Configuration
97
+
98
+ All configuration is via environment variables. Pass them through the `env` dict in `claude_desktop_config.json` (for Claude Desktop) or set them in `.env` for local development.
99
+
100
+ | Env var | Default | Description |
101
+ |---------|---------|-------------|
102
+ | `MACWHISPER_ALLOWED_PATHS` | `~/Desktop` | Colon-separated list of directories the server may read from |
103
+ | `MACWHISPER_CLI` | auto-detected | Path to the `mw` binary. Defaults to `/Applications/MacWhisper.app/Contents/MacOS/mw` if that file exists, otherwise `mw` on `PATH` |
104
+ | `MACWHISPER_LOG_PATH` | `~/Library/Logs/macwhisper-mcp.log` | Log file path (never stdout — that's reserved for MCP) |
105
+
106
+ **Local development:** copy `.env.example` to `.env` and adjust. With [direnv](https://direnv.net/), `.envrc` exports `.env` automatically. Without direnv: `source .env`.
107
+
108
+ ---
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ source .venv/bin/activate
114
+ pip install -e ".[dev]"
115
+
116
+ # Tests
117
+ pytest -q
118
+
119
+ # Lint + format
120
+ ruff check .
121
+ ruff format .
122
+
123
+ # Pre-commit hooks (one-time setup)
124
+ pip install pre-commit
125
+ pre-commit install
126
+
127
+ # Smoke-test against a real audio file (server must not be running in Claude Desktop)
128
+ python scripts/smoke_test.py ~/Downloads/Test.m4a
129
+ ```
130
+
131
+ ### Logs
132
+
133
+ ```bash
134
+ tail -f ~/Library/Logs/macwhisper-mcp.log
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Security
140
+
141
+ - All file paths are resolved (symlinks followed) and checked against the `MACWHISPER_ALLOWED_PATHS` allow-list before anything reaches the CLI.
142
+ - `subprocess.run` is always called with an argv list — never `shell=True`.
143
+ - No network calls. Ever.
144
+
145
+ See [PRD §7](./PRD.md) for the full threat model.
146
+
147
+ ---
148
+
149
+ ## Known limitations
150
+
151
+ - **Danish letter names:** Whisper may phonetically approximate letter names (e.g. "Æ, Ø, Å" → "E, Y, U") when they are spoken in isolation. Letters *inside words* transcribe correctly. This is a Whisper engine limitation, not a bug in this wrapper. See [PRD §12](./PRD.md).
152
+ - **Cold-start latency:** First transcription after MacWhisper launches takes ~13s (model load). Subsequent calls are ~2s.
153
+
154
+ ---
155
+
156
+ ## License
157
+
158
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "macwhisper-mcp-server"
7
+ version = "1.0.0"
8
+ description = "Local MCP server exposing MacWhisper transcription to Claude Desktop."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10,<3.14"
11
+ license = "MIT"
12
+ authors = [{ name = "Thomas" }]
13
+ keywords = ["mcp", "macwhisper", "transcription", "claude", "llm"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Environment :: MacOS X",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: MacOS",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
22
+ ]
23
+ dependencies = [
24
+ "fastmcp==3.2.4",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=8.0",
30
+ "pytest-mock>=3.12",
31
+ "ruff>=0.6",
32
+ ]
33
+
34
+ [project.scripts]
35
+ macwhisper-mcp = "macwhisper_mcp.server:main"
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/docdyhr/macwhisper-mcp-server"
39
+ Issues = "https://github.com/docdyhr/macwhisper-mcp-server/issues"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.ruff]
45
+ line-length = 100
46
+ target-version = "py313"
47
+ src = ["src", "tests"]
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
51
+ ignore = []
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ pythonpath = ["src"]
56
+ addopts = "-ra --strict-markers"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """macwhisper-mcp-server — local MCP server exposing MacWhisper transcription."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,61 @@
1
+ """Configuration loaded from environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ DEFAULT_ALLOWED = str(Path.home() / "Desktop")
10
+ DEFAULT_LOG_PATH = str(Path.home() / "Library" / "Logs" / "macwhisper-mcp.log")
11
+
12
+ # MacWhisper ships its CLI binary inside the app bundle and does not put it on PATH
13
+ # by default. Prefer the app-bundle path if present, fall back to `mw` on PATH.
14
+ _BUNDLED_CLI = Path("/Applications/MacWhisper.app/Contents/MacOS/mw")
15
+ DEFAULT_CLI = str(_BUNDLED_CLI) if _BUNDLED_CLI.exists() else "mw"
16
+
17
+ # File extensions accepted by MacWhisper. Reject anything else before invoking the CLI.
18
+ ALLOWED_EXTENSIONS: frozenset[str] = frozenset(
19
+ {".m4a", ".mp3", ".mp4", ".mov", ".wav", ".aiff", ".flac"}
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Config:
25
+ allowed_paths: tuple[Path, ...]
26
+ mw_cli: str = DEFAULT_CLI
27
+ log_path: Path = field(default_factory=lambda: Path(DEFAULT_LOG_PATH))
28
+
29
+ @classmethod
30
+ def from_env(cls) -> Config:
31
+ raw = os.environ.get("MACWHISPER_ALLOWED_PATHS", DEFAULT_ALLOWED)
32
+ allowed = tuple(Path(p).expanduser().resolve() for p in raw.split(":") if p.strip())
33
+ if not allowed:
34
+ raise ValueError("MACWHISPER_ALLOWED_PATHS must contain at least one directory.")
35
+
36
+ log_path_str = os.environ.get("MACWHISPER_LOG_PATH", DEFAULT_LOG_PATH)
37
+ log_path = Path(log_path_str).expanduser()
38
+ home = Path.home()
39
+ # resolve() without strict handles non-existent paths via lexical .. collapsing.
40
+ if home not in log_path.resolve().parents:
41
+ raise ValueError(
42
+ f"MACWHISPER_LOG_PATH must be inside your home directory ({home}). Got: {log_path}"
43
+ )
44
+
45
+ return cls(
46
+ allowed_paths=allowed,
47
+ mw_cli=os.environ.get("MACWHISPER_CLI", DEFAULT_CLI),
48
+ log_path=log_path,
49
+ )
50
+
51
+ def is_path_allowed(self, path: Path) -> bool:
52
+ """True if `path` resolves inside any of the allow-listed directories.
53
+
54
+ Uses `Path.resolve()` to follow symlinks before the prefix check, so a symlink
55
+ inside an allowed dir pointing outside is still rejected.
56
+ """
57
+ try:
58
+ resolved = path.resolve(strict=True)
59
+ except (FileNotFoundError, RuntimeError):
60
+ return False
61
+ return any(resolved == base or base in resolved.parents for base in self.allowed_paths)
@@ -0,0 +1,139 @@
1
+ """FastMCP server entry point.
2
+
3
+ Run via `python -m macwhisper_mcp.server` or the `macwhisper-mcp` console script.
4
+ Communicates over stdio — do NOT print to stdout anywhere. All logs go to a file.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import subprocess
11
+ import threading
12
+ from pathlib import Path
13
+
14
+ from fastmcp import FastMCP
15
+
16
+ from .config import Config
17
+ from .transcribe import TranscribeError, transcribe
18
+ from .watcher import FolderWatcher
19
+
20
+
21
+ def _setup_logging(config: Config) -> None:
22
+ config.log_path.parent.mkdir(parents=True, exist_ok=True)
23
+ logging.basicConfig(
24
+ filename=str(config.log_path),
25
+ level=logging.INFO,
26
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
27
+ )
28
+
29
+
30
+ def build_server(config: Config | None = None) -> FastMCP:
31
+ config = config or Config.from_env()
32
+ _setup_logging(config)
33
+ log = logging.getLogger(__name__)
34
+ log.info("Starting macwhisper-mcp-server, allow-list=%s", config.allowed_paths)
35
+
36
+ mcp = FastMCP("macwhisper")
37
+
38
+ # --- concurrency + cancel state ---
39
+ _transcribe_lock = threading.Lock()
40
+ _current_proc: list[subprocess.Popen] = [] # at most one element
41
+
42
+ # --- watch-folder state ---
43
+ _watcher: list[FolderWatcher] = [] # at most one element
44
+
45
+ @mcp.tool()
46
+ def transcribe_audio(path: str, model: str | None = None) -> str:
47
+ """Transcribe a local audio file using MacWhisper and return the transcript.
48
+
49
+ Args:
50
+ path: Absolute path to an audio file inside the configured allow-list.
51
+ Supported formats: m4a, mp3, mp4, mov, wav, aiff, flac.
52
+ model: Optional model override in MacWhisper engine:model-id format,
53
+ e.g. "whisperkit:openai_whisper-large-v3-v20240930" or
54
+ "parakeet-pro:nvidia_parakeet-v3_494MB". Defaults to the model
55
+ currently selected in MacWhisper.
56
+
57
+ Returns:
58
+ The full transcript as plain text.
59
+ """
60
+ if not _transcribe_lock.acquire(blocking=False):
61
+ raise TranscribeError(
62
+ "Busy: another transcription is already running. Try again shortly."
63
+ )
64
+ _current_proc.clear()
65
+ try:
66
+ return transcribe(path, config, model=model, _proc_ref=_current_proc)
67
+ except TranscribeError:
68
+ log.warning("transcribe_audio failed: %s", path)
69
+ raise
70
+ finally:
71
+ _current_proc.clear()
72
+ _transcribe_lock.release()
73
+
74
+ @mcp.tool()
75
+ def cancel_transcription() -> str:
76
+ """Cancel the currently running transcription, if any."""
77
+ if not _current_proc:
78
+ return "No transcription is currently running."
79
+ _current_proc[0].kill()
80
+ log.info("Transcription cancelled by user")
81
+ return "Transcription cancelled."
82
+
83
+ @mcp.tool()
84
+ def list_allowed_paths() -> list[str]:
85
+ """Return the directories this server is allowed to read audio from."""
86
+ return [str(p) for p in config.allowed_paths]
87
+
88
+ @mcp.tool()
89
+ def start_watch(folder: str) -> str:
90
+ """Start watching a folder for new audio files to auto-transcribe.
91
+
92
+ New audio files dropped into ``folder`` are transcribed automatically
93
+ and moved to ``<folder>/../done/``. Call ``get_watch_results()`` to
94
+ retrieve completed transcriptions.
95
+
96
+ Args:
97
+ folder: Absolute or ``~``-prefixed path to the incoming directory.
98
+ """
99
+ if _watcher and _watcher[0].is_running:
100
+ return f"Already watching {_watcher[0].incoming}. Call stop_watch() first."
101
+ incoming = Path(folder).expanduser().resolve()
102
+ if not any(incoming == base or base in incoming.parents for base in config.allowed_paths):
103
+ raise TranscribeError("Access denied: folder is outside the configured allow-list.")
104
+ w = FolderWatcher(incoming, config)
105
+ w.start()
106
+ if _watcher:
107
+ _watcher[0] = w
108
+ else:
109
+ _watcher.append(w)
110
+ return f"Watching {w.incoming} — completed files moved to {w.done_dir}"
111
+
112
+ @mcp.tool()
113
+ def stop_watch() -> str:
114
+ """Stop the active folder watcher."""
115
+ if not _watcher or not _watcher[0].is_running:
116
+ return "No active watcher."
117
+ w = _watcher[0]
118
+ w.stop()
119
+ return f"Stopped watching {w.incoming}"
120
+
121
+ @mcp.tool()
122
+ def get_watch_results() -> list[dict]:
123
+ """Return completed watch-folder transcriptions and clear the queue.
124
+
125
+ Each entry contains: ``file``, ``transcript``, ``destination``, ``error``.
126
+ """
127
+ if not _watcher:
128
+ return []
129
+ return _watcher[0].drain_results()
130
+
131
+ return mcp
132
+
133
+
134
+ def main() -> None:
135
+ build_server().run()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()