diff --git a/mcp-servers/project_health/README.md b/mcp-servers/project_health/README.md deleted file mode 100644 index f9c6237..0000000 --- a/mcp-servers/project_health/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# MCP Server: project_health - -Analyzes codebase health, including statistics, markers (TODO/FIXME), secrets detection, duplicate files, and dependency analysis. - -## Tools - -TODO: list tools and their purposes. - -## Setup - -```bash -python -m venv .venv -source .venv/bin/activate -pip install -e . -``` - -## Navi registration - -Create a file `mcp_servers.d/project_health.json` with the server config: -- transport: stdio -- command: absolute path to `.venv/bin/python` -- args: `["-m", "app.mcp_server"]` -- cwd: absolute path to this directory -- groups: map tool names to logical groups diff --git a/mcp-servers/project_health/app/__init__.py b/mcp-servers/project_health/app/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mcp-servers/project_health/app/__init__.py +++ /dev/null diff --git a/mcp-servers/project_health/app/mcp_server.py b/mcp-servers/project_health/app/mcp_server.py deleted file mode 100644 index fb64e07..0000000 --- a/mcp-servers/project_health/app/mcp_server.py +++ /dev/null @@ -1,233 +0,0 @@ -"""MCP server for project_health — Analyzes project structure, finds duplicates, and detects dependencies.""" - -from __future__ import annotations - -import hashlib -import json -import os -import re -from pathlib import Path -from typing import Annotated, Any - -from mcp.server.fastmcp import FastMCP -from pydantic import Field - -INSTRUCTIONS = """ -project_health provides tools to analyze the health and structure of a codebase. - -Use it when the task involves: -- summarizing project statistics (files, lines, languages). -- finding TODO/FIXME markers or potential secrets in the codebase. -- identifying duplicate files based on content. -- detecting project dependencies from configuration files. - -Workflow: -1. get_project_summary — check the overall state and potential issues. -2. find_duplicate_files — clean up redundant files. -3. get_project_dependencies — understand the project's external requirements. - -ABSOLUTE RULE — NEVER bypass MCP tools: -You MUST NOT use filesystem, terminal, code_exec, or any direct file access for operations covered by this server. -""".strip() - -mcp = FastMCP("project_health", instructions=INSTRUCTIONS) - - -def _json(data: Any) -> str: - return json.dumps(data, ensure_ascii=False, indent=2) - - -# ── TOOL DEFINITIONS ──────────────────────────────────────── -# ALL @mcp.tool decorators MUST be placed here, BEFORE main(). - -@mcp.tool(name="get_project_summary") -async def get_project_summary( - path: Annotated[str, Field(description="Absolute path to the project root.")], -) -> str: - """Summarize project stats, markers (TODO/FIXME), and potential secrets.""" - root = Path(path) - if not root.is_dir(): - return _json({"error": f"Path {path} is not a directory."}) - - stats = {"total_files": 0, "total_lines": 0, "languages": {}} - markers = [] - secrets_found = [] - - # Patterns for secrets - secret_patterns = { - "API Key": re.compile(r"(?i)(api[_-]?key|token|secret|password|auth)[\\s:=]+['\"][a-zA-Z0-9]{16,}[\'\"]"), - "Generic Secret": re.compile(r"(?i)password\s*=\s*['\"][^'\"]+['\"]"), - } - - exclude_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv", ".pytest_cache", "dist", "build"} - - for dirpath, dirnames, filenames in os.walk(root): - # Prune excluded directories - dirnames[:] = [d for d in dirnames if d not in exclude_dirs] - - for filename in filenames: - file_path = Path(dirpath) / filename - try: - # Skip binary files or very large files for summary - if file_path.stat().st_size > 1_000_000: # 1MB limit for scanning - continue - - stats["total_files"] += 1 - - # Determine language by extension - ext = file_path.suffix.lower() - if ext in ['.py']: lang = 'Python' - elif ext in ['.js', '.ts']: lang = 'JavaScript/TypeScript' - elif ext in ['.md']: lang = 'Markdown' - elif ext in ['.json']: lang = 'JSON' - elif ext in ['.toml']: lang = 'TOML' - elif ext in ['.c', '.cpp', '.h']: lang = 'C/C++' - else: lang = 'Other' - - stats["languages"][lang] = stats["languages"].get(lang, 0) + 1 - - # Read content for markers and secrets - with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: - lines = f.readlines() - stats["total_lines"] += len(lines) - - for i, line in enumerate(lines, 1): - # Check for TODO/FIXME - if "TODO" in line or "FIXME" in line: - markers.append({ - "file": str(file_path.relative_to(root)), - "line": i, - "content": line.strip() - }) - - # Check for secrets - for name, pattern in secret_patterns.items(): - if pattern.search(line): - secrets_found.append({ - "file": str(file_path.relative_to(root)), - "line": i, - "type": name - }) - except Exception: - continue - - return _json({ - "file_stats": stats, - "markers": markers, - "secrets_found": secrets_found - }) - - -@mcp.tool(name="find_duplicate_files") -async def find_duplicate_files( - path: Annotated[str, Field(description="Absolute path to the project root.")], -) -> str: - """Find files with identical content using SHA256 hashing.""" - root = Path(path) - if not root.is_dir(): - return _json({"error": f"Path {path} is not a directory."}) - - hashes = {} # hash -> [list of paths] - exclude_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv"} - - for dirpath, dirnames, filenames in os.walk(root): - dirnames[:] = [d for d in dirnames if d not in exclude_dirs] - - for filename in filenames: - file_path = Path(dirpath) / filename - try: - # Only hash files up to 5MB to avoid performance issues - if file_path.stat().st_size > 5_000_000: - continue - - hasher = hashlib.sha256() - with open(file_path, 'rb') as f: - while chunk := f.read(8192): - hasher.update(chunk) - - file_hash = hasher.hexdigest() - rel_path = str(file_path.relative_to(root)) - - if file_hash in hashes: - hashes[file_hash].append(rel_path) - else: - hashes[file_hash] = [rel_path] - except Exception: - continue - - # Filter only groups that have more than one file - duplicates = [paths for paths in hashes.values() if len(paths) > 1] - return _json({"duplicate_groups": duplicates}) - - -@mcp.tool(name="get_project_dependencies") -async def get_project_dependencies( - path: Annotated[str, Field(description="Absolute path to the project root.")], -) -> str: - """Identify dependencies by parsing common configuration files.""" - root = Path(path) - if not root.is_dir(): - return _json({"error": f"Path {path} is not a directory."}) - - dependencies = { - "python": [], - "javascript": [], - "other": [] - } - - # Check pyproject.toml - pyproject = root / "pyproject.toml" - if pyproject.exists(): - try: - content = pyproject.read_text(encoding='utf-8') - # Simple regex to find dependencies in pyproject.toml - deps = re.findall(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL) - if deps: - # Clean up the matches - dep_list = [d.strip().strip('"').strip("'") for d in re.split(r',', deps[0])] - dependencies["python"].extend([d for d in dep_list if d]) - except Exception: - pass - - # Check requirements.txt - req_txt = root / "requirements.txt" - if req_txt.exists(): - try: - content = req_txt.read_text(encoding='utf-8') - deps = [line.strip() for line in content.splitlines() if line.strip() and not line.startswith("#")] - dependencies["python"].extend(deps) - except Exception: - pass - - # Check package.json - package_json = root / "package.json" - if package_json.exists(): - try: - data = json.loads(package_json.read_text(encoding='utf-8')) # Note: error handling needed - # This is a simplified parser - deps = data.get("dependencies", {}) - dev_deps = data.get("devDependencies", {}) - dependencies["javascript"].extend(list(deps.keys()) + list(dev_deps.keys())) - except Exception: - # Fallback to simple regex if JSON is messy or encoding fails - try: - content = package_json.read_text(encoding='utf-8') - deps = re.findall(r'"([^"]+)":\s*"[^"]*"', content) - dependencies["javascript"].extend(deps) - except Exception: - pass - - return _json(dependencies) - - - - -# ── MAIN / TRANSPORT ────────────────────────────────────────────────── - -def main() -> None: - transport = os.environ.get("MCP_TRANSPORT", "stdio") - mcp.run(transport=transport) - - -if __name__ == "__main__": - main() diff --git a/mcp-servers/project_health/pyproject.toml b/mcp-servers/project_health/pyproject.toml deleted file mode 100644 index 5d0c2d9..0000000 --- a/mcp-servers/project_health/pyproject.toml +++ /dev/null @@ -1,21 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "mcp-server-project_health" -version = "0.1.0" -description = "Analyzes codebase health, including statistics, markers (TODO/FIXME), secrets detection, duplicate files, and dependency analysis." -requires-python = ">=3.11" -dependencies = [ - "mcp>=1.27", - "pydantic>=2.0", - -] - -[project.scripts] -mcp-server-project_health = "app.mcp_server:main" - -[tool.setuptools.packages.find] -where = ["."] -include = ["app*"] diff --git a/mcp-servers/time_toolkit/README.md b/mcp-servers/time_toolkit/README.md deleted file mode 100644 index 0b751f1..0000000 --- a/mcp-servers/time_toolkit/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# MCP Server: time_toolkit - -Utilities for formatting, calculating differences, time arithmetic, and parsing natural language dates. - -## Tools - -TODO: list tools and their purposes. - -## Setup - -```bash -python -m venv .venv -source .venv/bin/activate -pip install -e . -``` - -## Navi registration - -Create a file `mcp_servers.d/time_toolkit.json` with the server config: -- transport: stdio -- command: absolute path to `.venv/bin/python` -- args: `["-m", "app.mcp_server"]` -- cwd: absolute path to this directory -- groups: map tool names to logical groups diff --git a/mcp-servers/time_toolkit/app/__init__.py b/mcp-servers/time_toolkit/app/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mcp-servers/time_toolkit/app/__init__.py +++ /dev/null diff --git a/mcp-servers/time_toolkit/app/mcp_server.py b/mcp-servers/time_toolkit/app/mcp_server.py deleted file mode 100644 index 0f130d5..0000000 --- a/mcp-servers/time_toolkit/app/mcp_server.py +++ /dev/null @@ -1,270 +0,0 @@ -"""MCP server for time_toolkit — A toolkit for advanced datetime manipulation and natural language parsing.""" - -from __future__ import annotations - -import json -import os -import re -from datetime import datetime, timedelta, timezone -from enum import Enum -from typing import Annotated, Any -from zoneinfo import ZoneInfo - -from mcp.server.fastmcp import FastMCP -from pydantic import Field - -INSTRUCTIONS = """ -time_toolkit provides tools for advanced datetime manipulation and natural language parsing. - -Use it when the task involves: -- Formatting ISO strings into various human-readable or standard formats. -- Calculating the duration between two timestamps in different units. -- Adding or subtracting time intervals from a specific datetime. -- Parsing natural language time expressions (e.g., "tomorrow", "in 2 hours") into ISO timestamps. - -Workflow: -1. parse_natural — convert natural language to an ISO string. -2. format_datetime — format the resulting ISO string for display. -3. add_time — perform arithmetic on the datetime. -4. calculate_duration — find the difference between two points in time. - -ABSOLUTE RULE — NEVER bypass MCP tools: -You MUST NOT use filesystem, terminal, code_exec, or any direct file access for operations covered by this server. -""".strip() - -mcp = FastMCP("time_toolkit", instructions=INSTRUCTIONS) - - -def _json(data: Any) -> str: - return json.dumps(data, ensure_ascii=False, indent=2) - - -class OutputFormat(str, Enum): - HUMAN = "human" - ISO = "iso" - RFC2822 = "rfc2822" - SHORT = "short" - - -class DurationUnit(str, Enum): - SECONDS = "seconds" - MINUTES = "minutes" - HOURS = "hours" - DAYS = "days" - WEEKS = "weeks" - MONTHS = "months" - YEARS = "years" - - -# ── TOOL DEFINITIONS ── - -@mcp.tool(name="format_datetime") -async def format_datetime( - iso_string: Annotated[str, Field(description="Input ISO 8601 string.")], - output_format: Annotated[OutputFormat, Field(description="The desired output format: 'human', 'iso', 'rfc2822', or 'short'.")], - target_timezone: Annotated[str, Field(description="IANA timezone name (e.g., 'UTC', 'Europe/Moscow'). Default is 'UTC'.")] = "UTC", -) -> str: - """Formats an ISO 8601 string into a specified format and timezone.""" - try: - dt_str = iso_string.replace("Z", "+00:00") - dt = datetime.fromisoformat(dt_str) - - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - - tz = ZoneInfo(target_timezone) - dt_localized = dt.astimezone(tz) - - if output_format == OutputFormat.ISO: - formatted = dt_localized.isoformat() - elif output_format == OutputFormat.RFC2822: - formatted = dt_localized.strftime("%a, %d %b %Y %H:%M:%S %z") - elif output_format == OutputFormat.SHORT: - formatted = dt_localized.strftime("%d.%m.%Y %H:%M") - else: # human - formatted = dt_localized.strftime("%d %B %Y, %H:%M") - - return _json({ - "formatted": formatted, - "timezone": target_timezone, - "original_iso": iso_string - }) - except Exception as e: - return _json({"error": str(e)}) - - -@mcp.tool(name="calculate_duration") -async def calculate_duration( - start_iso: Annotated[str, Field(description="Start ISO 8601 string.")], - end_iso: Annotated[str, Field(description="End ISO 8601 string.")], - unit: Annotated[DurationUnit, Field(description="The unit for the duration value.")] -) -> str: - """Calculates the duration between two ISO 8601 timestamps in the specified unit.""" - try: - start_dt = datetime.fromisoformat(start_iso.replace("Z", "+00:00")) - end_dt = datetime.fromisoformat(end_iso.replace("Z", "+00:00")) - - diff = end_dt - start_dt - seconds = diff.total_seconds() - - if unit == DurationUnit.SECONDS: - value = seconds - elif unit == DurationUnit.MINUTES: - value = seconds / 60 - elif unit == DurationUnit.HOURS: - value = seconds / 3600 - elif unit == DurationUnit.DAYS: - value = seconds / 86400 - elif unit == DurationUnit.WEEKS: - value = seconds / (86400 * 7) - elif unit == DurationUnit.MONTHS: - value = seconds / (86400 * 30) - elif unit == DurationUnit.YEARS: - value = seconds / (86400 * 365) - else: - raise ValueError(f"Unsupported unit: {unit}") - - return _json({ - "value": round(float(value), 4), - "unit": unit.value, - "start_iso": start_iso, - "end_iso": end_iso - }) - except Exception as e: - return _json({"error": str(e)}) - - -@mcp.tool(name="add_time") -async def add_time( - iso_string: Annotated[str, Field(description="Base ISO 8601 string.")], - value: Annotated[int, Field(description="The amount of time to add (can be negative).")], - unit: Annotated[DurationUnit, Field(description="The unit of the value to add.")] -) -> str: - """Adds or subtracts a specified amount of time from an ISO 8601 timestamp.""" - try: - dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00")) - - if unit == DurationUnit.SECONDS: - delta = timedelta(seconds=value) - elif unit == Tuple[DurationUnit, str] if False else DurationUnit.MINUTES: - delta = timedelta(minutes=value) - elif unit == DurationUnit.MINUTES: - delta = timedelta(minutes=value) - elif unit == DurationUnit.HOURS: - delta = timedelta(hours=value) - elif unit == DurationUnit.DAYS: - delta = timedelta(days=value) - elif unit == DurationUnit.WEEKS: - delta = timedelta(weeks=value) - elif unit == DurationUnit.MONTHS: - delta = timedelta(days=value * 30) - elif unit == DurationUnit.YEARS: - delta = timedelta(days=value * 365) - else: - raise ValueError(f"Unsupported unit: {unit}") - - result_dt = dt + delta - op_str = f"{'+' if value >= 0 else ''}{value} {unit.value}" - - return _json({ - "result_iso": result_dt.isoformat(), - "original_iso": iso_string, - "operation": op_str - }) - except Exception as e: - return _json({"error": str(e)}) - - -@mcp.tool(name="parse_natural") -async def parse_natural( - text: Annotated[str, Field(description="Natural language time expression (e.g., 'tomorrow', 'in 2 hours').")], -) -> str: - """Parses natural language time expressions into ISO 8601 timestamps.""" - try: - now = datetime.now(timezone.utc) - text = text.lower().strip() - - result_dt = None - is_relative = False - - # Absolute simple cases - if text == "now": - result_dt = now - elif text == "today": - result_dt = now.replace(hour=0, minute=0, second=0, microsecond=0) - elif text == "tomorrow": - result_dt = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - elif text == "yesterday": - result_dt = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - - # Relative "in X units" - elif re.match(r"^in (\d+) (second|minute|hour|day|week|month|year)s?$", text): - match = re.match(r"^in (\d+) (second|minute|hour|day|week|month|year)s?$", text) - val = int(match.group(1)) - unit = match.group(2) - is_relative = True - if unit == "second": delta = timedelta(seconds=val) - elif unit == "minute": delta = timedelta(minutes=val) - elif unit == "hour": delta = timedelta(hours=val) - elif unit == "day": delta = timedelta(days=val) - elif unit == "week": delta = timedelta(weeks=val) - elif unit == "month": delta = timedelta(days=val * 30) - elif unit == "year": delta = timedelta(days=val * 365) - result_dt = now + delta - - # Relative "X units ago" - elif reint := re.match(r"^(\d+) (second|minute|hour|day|week|month|year)s? ago$", text): - match = reint - val = int(match.group(1)) - unit = match.group(2) - is_relative = True - if unit == "second": delta = timedelta(seconds=val) - elif unit == "minute": delta = timedelta(minutes=val) - elif unit == "hour": delta = timedelta(hours=val) - elif unit == "day": delta = timedelta(days=val) - elif unit == "week": delta = timedelta(weeks=val) - elif unit == "month": delta = timedelta(days=val * 30) - elif unit == "year": delta = timedelta(days=val * 365) - result_dt = now - delta - - # "next Monday", "last Friday" - elif "next" in text or "last" in text: - days_map = {"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3, "friday": 4, "saturday": 5, "sunday": 6} - parts = text.split() - day_name = parts[-1] - if day_name in days_map: - is_relative = True - target_day_idx = days_map[day_name] - current_day_idx = now.weekday() - - if "next" in text: - diff = (target_day_idx - current_day_idx) % 7 - if diff == 0: diff = 7 - result_dt = (now + timedelta(days=diff)).replace(hour=0, minute=0, second=0, microsecond=0) - elif "last" in text: - diff = (current_day_idx - target_day_idx) % 7 - if diff == 0: diff = 7 - result_dt = (now - timedelta(days=diff)).replace(hour=0, minute=0, second=0, microsecond=0) - - if result_dt: - return _json({ - "iso": resultint_dt.isoformat() if (resultint_dt := result_dt) else "", - "parsed_from": text, - "type": "relative" if is_relative else "absolute" - }) - else: - return _json({"error": "could not parse"}) - - except Exception as e: - return _json({"error": str(e)}) - - -# ── MAIN / TRANSPORT ── - -def main() -> None: - transport = os.environ.get("MCP_TRANSPORT", "stdio") - mcp.run(transport=transport) - - -if __name__ == "__main__": - main diff --git a/mcp-servers/time_toolkit/pyproject.toml b/mcp-servers/time_toolkit/pyproject.toml deleted file mode 100644 index 859a890..0000000 --- a/mcp-servers/time_toolkit/pyproject.toml +++ /dev/null @@ -1,21 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "mcp-server-time_toolkit" -version = "0.1.0" -description = "Utilities for formatting, calculating differences, time arithmetic, and parsing natural language dates." -requires-python = ">=3.11" -dependencies = [ - "mcp>=1.27", - "pydantic>=2.0", - -] - -[project.scripts] -mcp-server-time_toolkit = "app.mcp_server:main" - -[tool.setuptools.packages.find] -where = ["."] -include = ["app*"] diff --git a/mcp_servers.d/project_health.json b/mcp_servers.d/project_health.json deleted file mode 100644 index b8065e5..0000000 --- a/mcp_servers.d/project_health.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "transport": "stdio", - "command": "/home/gmikcon/Projects/navi-1/mcp-servers/project_health/.venv/bin/python", - "args": ["-m", "app.mcp_server"], - "cwd": "/home/gmikcon/Projects/navi-1/mcp-servers/project_health", - "env": { - "MCP_TRANSPORT": "stdio" - }, - "groups": { - "default": ["get_project_summary", "find_duplicate_files", "get_project_dependencies"] - } -} \ No newline at end of file diff --git a/mcp_servers.d/time_toolkit.json b/mcp_servers.d/time_toolkit.json deleted file mode 100644 index 0b30ec9..0000000 --- a/mcp_servers.d/time_toolkit.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "transport": "stdio", - "command": "/home/gmikcon/Projects/navi-1/mcp-servers/time_toolkit/.venv/bin/python", - "args": ["-m", "app.mcp_server"], - "cwd": "/home/gmikcon/Projects/navi-1/mcp-servers/time_toolkit", - "env": { - "MCP_TRANSPORT": "stdio" - }, - "groups": { - "time": [ - "format_datetime", - "calculate_duration", - "add_time", - "parse_natural" - ] - } -}