Mostly safe โ a couple of notes worth reading.
Scanned 5/3/2026, 6:20:04 PMยทCached resultยทFast Scanยท45 rulesยทHow we decide โ
AIVSS Score
Low
Severity Breakdown
0
critical
1
high
131
medium
62
low
MCP Server Information
Findings
This package receives a B grade with a safety score of 52/100, indicating moderate security concerns that warrant attention before deployment. The scan identified one high-severity issue alongside 131 medium-severity findings, primarily concentrated in resource exhaustion risks (58), readiness problems (62), and verbose error handling (33) that could expose sensitive information or enable denial-of-service attacks. While no critical vulnerabilities were detected, the combination of configuration gaps and resource management weaknesses suggests you should implement additional monitoring and hardening measures if you proceed with installation.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 21 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
21 of 21 findings
21 findings
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 209 | Dictionary mapping episode numbers to MfpEpisode objects. |
| 210 | """ |
| 211 | episodes = {} |
| 212 | root = ET.fromstring(rss_content) |
| 213 | |
| 214 | for item in root.findall(".//item"): |
| 215 | enclosure = item.find("enclosure") |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
MCP tool file registers a tool AND contains a destructive sink (fs.unlink, os.remove, shutil.rmtree, DROP/DELETE FROM/TRUNCATE/ UPDATE ... SET, HTTP DELETE/PUT/PATCH, subprocess.run, exec/spawn), but the file has no confirmation path โ no `elicit()` call, no `dry_run`/`dryRun` flag, no `idempotency_key`/`idempotencyKey`, no `confirmation`/`confirm` token. A destructive handler that fires on the first call with no reversibility signal is an autonomous-action hazard (MCP Top-10 R10). Add one of:
Evidence
| 1 | """Conversation tools for interactive voice interactions.""" |
| 2 | |
| 3 | import asyncio |
| 4 | import logging |
| 5 | import os |
| 6 | import time |
| 7 | import traceback |
| 8 | from typing import Optional, Literal, Tuple, Dict, Union |
| 9 | from pathlib import Path |
| 10 | from datetime import datetime |
| 11 | |
| 12 | import numpy as np |
| 13 | import sounddevice as sd |
| 14 | from scipy.io.wavfile import write |
| 15 | from pydub import AudioSegment |
| 16 | from openai import AsyncOpenAI |
| 17 | import httpx |
| 18 | |
| 19 | # Optional webrtcvad for silence detection |
| 20 | try: |
| 21 | import webrtcvad |
| 22 | VAD_AVAILABLE = True |
| 23 | except |
Remediation
Pick one confirmation mechanism and add it to the destructive handler: - make the client confirm via `elicit()` before touching the sink, - accept a `dry_run` / `dryRun` boolean input and branch off the sink when true (return what would change), - require an `idempotency_key` / `idempotencyKey` the caller provides on the first call and echoes back to commit, - require a `confirmation` / `confirm` token as an input field. If the destructive call is genuinely irrelevant to the tool
MCP tool file registers a tool AND contains a destructive sink (fs.unlink, os.remove, shutil.rmtree, DROP/DELETE FROM/TRUNCATE/ UPDATE ... SET, HTTP DELETE/PUT/PATCH, subprocess.run, exec/spawn), but the file has no confirmation path โ no `elicit()` call, no `dry_run`/`dryRun` flag, no `idempotency_key`/`idempotencyKey`, no `confirmation`/`confirm` token. A destructive handler that fires on the first call with no reversibility signal is an autonomous-action hazard (MCP Top-10 R10). Add one of:
Evidence
| 1 | """Uninstall tool for Whisper STT service.""" |
| 2 | |
| 3 | import os |
| 4 | import shutil |
| 5 | import subprocess |
| 6 | import platform |
| 7 | import logging |
| 8 | from pathlib import Path |
| 9 | from typing import Dict, Any, Union |
| 10 | |
| 11 | from voice_mode.server import mcp |
| 12 | from voice_mode.config import BASE_DIR |
| 13 | from voice_mode.utils.services.common import find_process_by_port |
| 14 | |
| 15 | logger = logging.getLogger("voicemode") |
| 16 | |
| 17 | |
| 18 | @mcp.tool() |
| 19 | async def whisper_uninstall( |
| 20 | remove_models: Union[bool, str] = False, |
| 21 | remove_all_data: Union[bool, str] = False |
| 22 | ) -> |
Remediation
Pick one confirmation mechanism and add it to the destructive handler: - make the client confirm via `elicit()` before touching the sink, - accept a `dry_run` / `dryRun` boolean input and branch off the sink when true (return what would change), - require an `idempotency_key` / `idempotencyKey` the caller provides on the first call and echoes back to commit, - require a `confirmation` / `confirm` token as an input field. If the destructive call is genuinely irrelevant to the tool
MCP tool file registers a tool AND contains a destructive sink (fs.unlink, os.remove, shutil.rmtree, DROP/DELETE FROM/TRUNCATE/ UPDATE ... SET, HTTP DELETE/PUT/PATCH, subprocess.run, exec/spawn), but the file has no confirmation path โ no `elicit()` call, no `dry_run`/`dryRun` flag, no `idempotency_key`/`idempotencyKey`, no `confirmation`/`confirm` token. A destructive handler that fires on the first call with no reversibility signal is an autonomous-action hazard (MCP Top-10 R10). Add one of:
Evidence
| 1 | """Uninstall tool for Kokoro TTS service.""" |
| 2 | |
| 3 | import os |
| 4 | import shutil |
| 5 | import subprocess |
| 6 | import platform |
| 7 | import logging |
| 8 | from pathlib import Path |
| 9 | from typing import Dict, Any, Union |
| 10 | |
| 11 | from voice_mode.server import mcp |
| 12 | from voice_mode.config import BASE_DIR |
| 13 | from voice_mode.utils.services.common import find_process_by_port |
| 14 | |
| 15 | logger = logging.getLogger("voicemode") |
| 16 | |
| 17 | |
| 18 | @mcp.tool() |
| 19 | async def kokoro_uninstall( |
| 20 | remove_models: Union[bool, str] = False, |
| 21 | remove_all_data: Union[bool, str] = False |
| 22 | ) -> D |
Remediation
Pick one confirmation mechanism and add it to the destructive handler: - make the client confirm via `elicit()` before touching the sink, - accept a `dry_run` / `dryRun` boolean input and branch off the sink when true (return what would change), - require an `idempotency_key` / `idempotencyKey` the caller provides on the first call and echoes back to commit, - require a `confirmation` / `confirm` token as an input field. If the destructive call is genuinely irrelevant to the tool
MCP tool file registers a tool AND contains a destructive sink (fs.unlink, os.remove, shutil.rmtree, DROP/DELETE FROM/TRUNCATE/ UPDATE ... SET, HTTP DELETE/PUT/PATCH, subprocess.run, exec/spawn), but the file has no confirmation path โ no `elicit()` call, no `dry_run`/`dryRun` flag, no `idempotency_key`/`idempotencyKey`, no `confirmation`/`confirm` token. A destructive handler that fires on the first call with no reversibility signal is an autonomous-action hazard (MCP Top-10 R10). Add one of:
Evidence
| 1 | """Installation tool for kokoro-fastapi TTS service.""" |
| 2 | |
| 3 | import os |
| 4 | import sys |
| 5 | import platform |
| 6 | import subprocess |
| 7 | import shutil |
| 8 | import logging |
| 9 | from pathlib import Path |
| 10 | from typing import Dict, Any, Optional, Union |
| 11 | import asyncio |
| 12 | import aiohttp |
| 13 | |
| 14 | from voice_mode.server import mcp |
| 15 | from voice_mode.config import SERVICE_AUTO_ENABLE |
| 16 | from voice_mode.utils.version_helpers import ( |
| 17 | get_git_tags, get_latest_stable_tag, get_current_version, |
| 18 | checkout_version, is_version_installed |
| 19 | ) |
| 20 | from voice_mode.uti |
Remediation
Pick one confirmation mechanism and add it to the destructive handler: - make the client confirm via `elicit()` before touching the sink, - accept a `dry_run` / `dryRun` boolean input and branch off the sink when true (return what would change), - require an `idempotency_key` / `idempotencyKey` the caller provides on the first call and echoes back to commit, - require a `confirmation` / `confirm` token as an input field. If the destructive call is genuinely irrelevant to the tool
MCP tool file registers a tool AND contains a destructive sink (fs.unlink, os.remove, shutil.rmtree, DROP/DELETE FROM/TRUNCATE/ UPDATE ... SET, HTTP DELETE/PUT/PATCH, subprocess.run, exec/spawn), but the file has no confirmation path โ no `elicit()` call, no `dry_run`/`dryRun` flag, no `idempotency_key`/`idempotencyKey`, no `confirmation`/`confirm` token. A destructive handler that fires on the first call with no reversibility signal is an autonomous-action hazard (MCP Top-10 R10). Add one of:
Evidence
| 1 | """Installation tool for whisper.cpp""" |
| 2 | |
| 3 | import os |
| 4 | import sys |
| 5 | import platform |
| 6 | import subprocess |
| 7 | import shutil |
| 8 | import json |
| 9 | import logging |
| 10 | from pathlib import Path |
| 11 | from typing import Dict, Any, Optional, Union |
| 12 | import asyncio |
| 13 | import aiohttp |
| 14 | try: |
| 15 | from importlib.resources import files |
| 16 | except ImportError: |
| 17 | # Python < 3.9 fallback |
| 18 | from importlib_resources import files |
| 19 | |
| 20 | from voice_mode.server import mcp |
| 21 | from voice_mode.config import SERVICE_AUTO_ENABLE, DEFAULT_WHISPER_MODEL, WHISPER_PORT |
| 22 | fro |
Remediation
Pick one confirmation mechanism and add it to the destructive handler: - make the client confirm via `elicit()` before touching the sink, - accept a `dry_run` / `dryRun` boolean input and branch off the sink when true (return what would change), - require an `idempotency_key` / `idempotencyKey` the caller provides on the first call and echoes back to commit, - require a `confirmation` / `confirm` token as an input field. If the destructive call is genuinely irrelevant to the tool
MCP tool file registers a tool AND contains a destructive sink (fs.unlink, os.remove, shutil.rmtree, DROP/DELETE FROM/TRUNCATE/ UPDATE ... SET, HTTP DELETE/PUT/PATCH, subprocess.run, exec/spawn), but the file has no confirmation path โ no `elicit()` call, no `dry_run`/`dryRun` flag, no `idempotency_key`/`idempotencyKey`, no `confirmation`/`confirm` token. A destructive handler that fires on the first call with no reversibility signal is an autonomous-action hazard (MCP Top-10 R10). Add one of:
Evidence
| 1 | """Unified service management tool for voice mode services.""" |
| 2 | |
| 3 | import asyncio |
| 4 | import logging |
| 5 | import os |
| 6 | import platform |
| 7 | import subprocess |
| 8 | import time |
| 9 | from pathlib import Path |
| 10 | from typing import Literal, Optional, Dict, Any, Union |
| 11 | |
| 12 | import psutil |
| 13 | |
| 14 | from voice_mode.server import mcp |
| 15 | from voice_mode.config import WHISPER_PORT, KOKORO_PORT, MLX_AUDIO_PORT, SERVICE_AUTO_ENABLE |
| 16 | from voice_mode.utils.services.common import find_process_by_port, check_service_status |
| 17 | |
| 18 | # Default port for VoiceMode serve com |
Remediation
Pick one confirmation mechanism and add it to the destructive handler: - make the client confirm via `elicit()` before touching the sink, - accept a `dry_run` / `dryRun` boolean input and branch off the sink when true (return what would change), - require an `idempotency_key` / `idempotencyKey` the caller provides on the first call and echoes back to commit, - require a `confirmation` / `confirm` token as an input field. If the destructive call is genuinely irrelevant to the tool
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 239 | success = event['data'].get('success', True) |
| 240 | details = f"tool={tool}, success={success}" |
| 241 | |
| 242 | print(f"{elapsed:>7.3f}s {event['event_type']:25} {details}") |
| 243 | |
| 244 | |
| 245 | def calculate_ai_thinking_times(all_events: List[Dict[str, Any]]) -> List[float]: |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 20 | if __name__ == "__main__": |
| 21 | if len(sys.argv) != 3: |
| 22 | print("Usage: process-readme-for-docs.py <input> <output>") |
| 23 | sys.exit(1) |
| 24 | |
| 25 | process_readme(sys.argv[1], sys.argv[2]) |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 181 | event = data.decode('utf-8', errors='ignore') |
| 182 | if event.endswith('_1'): # Touch needed |
| 183 | print(f"YubiKey touch needed: {event}") |
| 184 | play_notification() |
| 185 | |
| 186 | except Exception as e: |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 25 | result = subprocess.run(cmd, shell=shell, capture_output=True, text=True) |
| 26 | return result.returncode == 0, result.stdout, result.stderr |
| 27 | except Exception as e: |
| 28 | return False, "", str(e) |
| 29 | |
| 30 | def check_wsl_version(): |
| 31 | """Check if running in WSL and get version info""" |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 885 | except Exception as e: |
| 886 | logger.error(f"Error viewing logs for {service_name}: {e}") |
| 887 | return f"โ Error viewing logs: {str(e)}" |
| 888 | |
| 889 | |
| 890 | @mcp.tool() |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 645 | except Exception as e: |
| 646 | logger.error(f"Error stopping {service_name}: {e}") |
| 647 | return f"โ Error stopping {service_name}: {str(e)}" |
| 648 | |
| 649 | |
| 650 | async def restart_service(service_name: str) -> str: |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 216 | except Exception as e: |
| 217 | logger.error(f"Error showing recent statistics: {e}") |
| 218 | return f"Error showing recent statistics: {str(e)}" |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 22 | try: |
| 23 | return path.read_text() |
| 24 | except Exception as e: |
| 25 | return f"Error reading CHANGELOG.md from {path}: {str(e)}" |
| 26 | |
| 27 | return """CHANGELOG.md not found in package. |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure โ useful recon for attackers.
Evidence
| 88 | except Exception as e: |
| 89 | logger.error(f"Error generating statistics summary: {e}") |
| 90 | return f"Error generating statistics summary: {str(e)}" |
| 91 | |
| 92 | |
| 93 | @mcp.tool() |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 95 | info["running"] = True |
| 96 | info["pid"] = proc.pid |
| 97 | try: |
| 98 | info["uptime_seconds"] = int(proc.create_time()) |
| 99 | except: |
| 100 | pass |
| 101 | |
| 102 | except Exception as e: |
| 103 | info["error"] = str(e) |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 88 | ["launchctl", "remove", service_name], |
| 89 | capture_output=True |
| 90 | ) |
| 91 | removed_items.append(f"Removed launchctl instance: {service_name}") |
| 92 | except Exception: |
| 93 | pass |
| 94 | |
| 95 | elif system == "Linux": |
| 96 | # Remove systemd service |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 265 | _play_audio(tmp_path) |
| 266 | finally: |
| 267 | try: |
| 268 | os.unlink(tmp_path) |
| 269 | except OSError: |
| 270 | pass |
| 271 | |
| 272 | |
| 273 | def _sayas_preview(voice: str, profile: dict) -> None: |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 3341 | if not track and track_path.is_absolute(): |
| 3342 | try: |
| 3343 | rel_path = str(track_path.relative_to(library.music_root)) |
| 3344 | track = library.get_track_by_path(rel_path) |
| 3345 | except ValueError: |
| 3346 | pass |
| 3347 | |
| 3348 | if not track: |
| 3349 | click.echo(f"Track not found in library: {status.path}", err=True) |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 98 | ) |
| 99 | if result.returncode == 0: |
| 100 | data = json.loads(result.stdout) |
| 101 | return data['info']['version'] |
| 102 | except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError, KeyError): |
| 103 | pass |
| 104 | return None |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
converse
whisper_uninstall
kokoro_uninstall
kokoro_install
whisper_install
whisper-server