from __future__ import annotations # Project by Nymbo import json import os import sys import threading import time from datetime import datetime, timedelta from typing import Any import gradio as gr class RateLimiter: """Best-effort in-process rate limiter for HTTP-heavy tools.""" def __init__(self, requests_per_minute: int = 30) -> None: self.requests_per_minute = requests_per_minute self._requests: list[datetime] = [] self._lock = threading.Lock() def acquire(self) -> None: now = datetime.now() with self._lock: self._requests = [req for req in self._requests if now - req < timedelta(minutes=1)] if len(self._requests) >= self.requests_per_minute: wait_time = 60 - (now - self._requests[0]).total_seconds() if wait_time > 0: time.sleep(max(1, wait_time)) self._requests.append(now) _search_rate_limiter = RateLimiter(requests_per_minute=20) _fetch_rate_limiter = RateLimiter(requests_per_minute=25) def _truncate_for_log(value: str, limit: int = 500) -> str: if len(value) <= limit: return value return value[: limit - 1] + "…" def _serialize_input(val: Any) -> Any: try: if isinstance(val, (str, int, float, bool)) or val is None: return val if isinstance(val, (list, tuple)): return [_serialize_input(v) for v in list(val)[:10]] + (["…"] if len(val) > 10 else []) if isinstance(val, dict): out: dict[str, Any] = {} for i, (k, v) in enumerate(val.items()): if i >= 12: out["…"] = "…" break out[str(k)] = _serialize_input(v) return out return repr(val)[:120] except Exception: return "" def _log_call_start(func_name: str, **kwargs: Any) -> None: try: compact = {k: _serialize_input(v) for k, v in kwargs.items()} print(f"[TOOL CALL] {func_name} inputs: {json.dumps(compact, ensure_ascii=False)[:800]}", flush=True) except Exception as exc: print(f"[TOOL CALL] {func_name} (failed to log inputs: {exc})", flush=True) def _log_call_end(func_name: str, output_desc: str) -> None: try: print(f"[TOOL RESULT] {func_name} output: {output_desc}", flush=True) except Exception as exc: print(f"[TOOL RESULT] {func_name} (failed to log output: {exc})", flush=True) # Ensure Tools modules can import 'app' when this file is executed as a script # (their code does `from app import ...`). sys.modules.setdefault("app", sys.modules[__name__]) # Import per-tool interface builders from the Tools package from Modules.Web_Fetch import build_interface as build_fetch_interface from Modules.Web_Search import build_interface as build_search_interface from Modules.Code_Interpreter import build_interface as build_code_interface from Modules.Memory_Manager import build_interface as build_memory_interface from Modules.Generate_Speech import build_interface as build_speech_interface from Modules.Generate_Image import build_interface as build_image_interface from Modules.Generate_Video import build_interface as build_video_interface from Modules.Deep_Research import build_interface as build_research_interface from Modules.File_System import build_interface as build_fs_interface from Modules.Obsidian_Vault import build_interface as build_obsidian_interface from Modules.Shell_Command import build_interface as build_shell_interface # Optional environment flags used to conditionally show API schemas (unchanged behavior) HF_IMAGE_TOKEN = bool(os.getenv("HF_READ_TOKEN")) HF_VIDEO_TOKEN = bool(os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN")) HF_TEXTGEN_TOKEN = bool(os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN")) # CSS copied from prior app.py to preserve exact look-and-feel CSS_STYLES = """ /* App background: subtle top-left glow, light coming from one side */ .gradio-container { /* Keep existing theme background but add a faint glow overlay */ background-image: radial-gradient(1200px 800px at 0% 0%, rgba(99, 102, 241, 0.10), rgba(99, 102, 241, 0.00) 70%), radial-gradient(700px 500px at 100% 0%, rgba(59, 130, 246, 0.05), rgba(59, 130, 246, 0.00) 70%); background-attachment: fixed, fixed; /* gentle parallax feel on scroll */ background-repeat: no-repeat; background-blend-mode: screen; /* subtle light effect over dark themes */ } @media (prefers-color-scheme: light) { .gradio-container { /* Slightly softer in light mode */ background-image: radial-gradient(1200px 800px at 0% 0%, rgba(99, 102, 241, 0.08), rgba(99, 102, 241, 0.00) 70%), radial-gradient(700px 500px at 100% 0%, rgba(59, 130, 246, 0.04), rgba(59, 130, 246, 0.00) 70%); background-blend-mode: multiply; /* keep gentle tint over light base */ } } /* Style only the top-level app title to avoid affecting headings elsewhere */ .app-title { text-align: center; /* Ensure main title appears first, then our two subtitle lines */ display: grid; justify-items: center; } .app-title::after { grid-row: 2; content: "General purpose tools useful for any agent."; display: block; font-size: 1rem; font-weight: 400; opacity: 0.9; margin-top: 2px; white-space: pre-wrap; } /* Historical safeguard: if any h1 appears inside tabs, don't attach pseudo content */ .gradio-container [role=\"tabpanel\"] h1::before, .gradio-container [role=\"tabpanel\"] h1::after { content: none !important; } /* Information accordion - modern info cards */ .info-accordion { margin: 8px 0 2px; } .info-grid { display: grid; gap: 12px; /* Force a 2x2 layout on medium+ screens */ grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; } /* On narrow screens, stack into a single column */ @media (max-width: 800px) { .info-grid { grid-template-columns: 1fr; } } .info-card { display: flex; gap: 14px; padding: 14px 16px; border: 1px solid rgba(255, 255, 255, 0.08); background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); border-radius: 12px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); position: relative; overflow: hidden; backdrop-filter: blur(2px); } .info-card::before { content: ""; position: absolute; inset: 0; border-radius: 12px; pointer-events: none; background: linear-gradient(90deg, rgba(99,102,241,0.06), rgba(59,130,246,0.05)); } .info-card__icon { font-size: 24px; flex: 0 0 28px; line-height: 1; filter: saturate(1.1); } .info-card__body { min-width: 0; } .info-card__body h3 { margin: 0 0 6px; font-size: 1.05rem; } .info-card__body p { margin: 6px 0; opacity: 0.95; } /* Readable code blocks inside info cards */ .info-card pre { margin: 8px 0; padding: 10px 12px; background: rgba(20, 20, 30, 0.55); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 10px; overflow-x: auto; white-space: pre; } .info-card code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 0.95em; } .info-card pre code { display: block; } .info-list { margin: 6px 0 0 18px; padding: 0; } .info-hint { margin-top: 8px; font-size: 0.9em; opacity: 0.9; } /* Light theme adjustments */ @media (prefers-color-scheme: light) { .info-card { border-color: rgba(0, 0, 0, 0.08); background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,0.9)); } .info-card::before { background: linear-gradient(90deg, rgba(99,102,241,0.08), rgba(59,130,246,0.06)); } .info-card pre { background: rgba(245, 246, 250, 0.95); border-color: rgba(0, 0, 0, 0.08); } } /* Tabs - modern, evenly distributed full-width buttons */ .gradio-container [role="tablist"] { display: flex; gap: 8px; flex-wrap: nowrap; align-items: stretch; width: 100%; } .gradio-container [role="tab"] { flex: 1 1 0; min-width: 0; /* allow shrinking to fit */ display: inline-flex; justify-content: center; align-items: center; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.08); background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .06s ease; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .gradio-container [role="tab"]:hover { border-color: rgba(99,102,241,0.28); background: linear-gradient(180deg, rgba(99,102,241,0.10), rgba(59,130,246,0.08)); } .gradio-container [role="tab"][aria-selected="true"] { border-color: rgba(99,102,241,0.35); box-shadow: inset 0 0 0 1px rgba(99,102,241,0.25), 0 1px 2px rgba(0,0,0,0.25); background: linear-gradient(180deg, rgba(99,102,241,0.18), rgba(59,130,246,0.14)); color: rgba(255, 255, 255, 0.95) !important; } .gradio-container [role="tab"]:active { transform: translateY(0.5px); } .gradio-container [role="tab"]:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(59,130,246,0.35); } @media (prefers-color-scheme: light) { .gradio-container [role="tab"] { border-color: rgba(0, 0, 0, 0.08); background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,0.90)); } .gradio-container [role="tab"]:hover { border-color: rgba(99,102,241,0.25); background: linear-gradient(180deg, rgba(99,102,241,0.08), rgba(59,130,246,0.06)); } .gradio-container [role="tab"][aria-selected="true"] { border-color: rgba(99,102,241,0.35); background: linear-gradient(180deg, rgba(99,102,241,0.16), rgba(59,130,246,0.12)); color: rgba(0, 0, 0, 0.85) !important; } } """ # Build each tab interface using modular builders fetch_interface = build_fetch_interface() concise_interface = build_search_interface() code_interface = build_code_interface() memory_interface = build_memory_interface() kokoro_interface = build_speech_interface() image_generation_interface = build_image_interface() video_generation_interface = build_video_interface() deep_research_interface = build_research_interface() fs_interface = build_fs_interface() shell_interface = build_shell_interface() obsidian_interface = build_obsidian_interface() _interfaces = [ fetch_interface, concise_interface, code_interface, shell_interface, fs_interface, obsidian_interface, memory_interface, kokoro_interface, image_generation_interface, video_generation_interface, deep_research_interface, ] _tab_names = [ "Web Fetch", "Web Search", "Code Interpreter", "Shell Command", "File System", "Obsidian Vault", "Memory Manager", "Generate Speech", "Generate Image", "Generate Video", "Deep Research", ] with gr.Blocks(title="Nymbo/Tools MCP", theme="Nymbo/Nymbo_Theme", css=CSS_STYLES) as demo: # Title and information panel unchanged to preserve UI gr.HTML("

Nymbo/Tools MCP

") with gr.Accordion("Information", open=False): gr.HTML( """
πŸ”

Enable Image Gen, Video Gen, and Deep Research

The Generate_Image, Generate_Video, and Deep_Research tools require a HF_READ_TOKEN set as a secret or environment variable.

  • Duplicate this Space and add a HF token with model read access.
  • Or run locally with HF_READ_TOKEN in your environment.
MCP clients can see these tools even without tokens, but calls will fail until a valid token is provided.
🧠

Persistent Memories and Files

In this public demo, memories and files created with the Memory_Manager and File_System are stored in the Space's running container and are cleared when the Space restarts. Content is visible to everyoneβ€”avoid personal data.

When running locally, memories are saved to memories.json at the repo root for privacy, and files are saved to the Tools/Filesystem directory on disk.

πŸ”—

Connecting from an MCP Client

This Space also runs as a Model Context Protocol (MCP) server. Point your client to:
https://mcp.nymbo.net/gradio_api/mcp/

Example client configuration:

{
  "mcpServers": {
    "nymbo-tools": {
      "url": "https://mcp.nymbo.net/gradio_api/mcp/"
    }
  }
}

Run the following commands in sequence to run the server locally:

git clone https://huggingface.co/spaces/Nymbo/Tools
cd Tools
python -m venv env
source env/bin/activate
pip install -r requirements.txt
python app.py
πŸ› οΈ

Tool Notes & Kokoro Voice Legend

No authentication required for: Web_Fetch, Web_Search, Code_Interpreter, Memory_Manager, Generate_Speech, File_System, and Shell_Command.

Kokoro voice prefixes

  • af β€” American female
  • am β€” American male
  • bf β€” British female
  • bm β€” British male
  • ef β€” European female
  • em β€” European male
  • hf β€” Hindi female
  • hm β€” Hindi male
  • if β€” Italian female
  • im β€” Italian male
  • jf β€” Japanese female
  • jm β€” Japanese male
  • pf β€” Portuguese female
  • pm β€” Portuguese male
  • zf β€” Chinese female
  • zm β€” Chinese male
  • ff β€” French female
""" ) gr.TabbedInterface(interface_list=_interfaces, tab_names=_tab_names) if __name__ == "__main__": demo.launch(mcp_server=True)