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: Any, limit: int = 500) -> str: if not isinstance(value, str): value = str(value) 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.Agent_Terminal import build_interface as build_agent_terminal_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 = """ /* 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; } /* Sidebar Container */ .app-sidebar { background-color: rgba(255, 255, 255, 0.02) !important; border-right: 1px solid rgba(255, 255, 255, 0.08) !important; } @media (prefers-color-scheme: light) { .app-sidebar { background-color: rgba(0, 0, 0, 0.02) !important; border-right: 1px solid rgba(0, 0, 0, 0.08) !important; } } /* 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-card p { word-wrap: break-word; overflow-wrap: break-word; } .info-card p code { word-break: break-all; } .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); } } /* Sidebar Navigation - styled like the previous tabs */ .sidebar-nav { background: transparent !important; border: none !important; padding: 0 !important; } .sidebar-nav .form { gap: 8px !important; display: flex !important; flex-direction: column !important; border: none !important; background: transparent !important; } .sidebar-nav label { display: flex !important; align-items: center !important; padding: 10px 12px !important; border-radius: 10px !important; border: 1px solid rgba(255, 255, 255, 0.08) !important; background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)) !important; transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .06s ease !important; cursor: pointer !important; margin-bottom: 0 !important; width: 100% !important; justify-content: flex-start !important; text-align: left !important; } .sidebar-nav label:hover { border-color: rgba(99,102,241,0.28) !important; background: linear-gradient(180deg, rgba(99,102,241,0.10), rgba(59,130,246,0.08)) !important; } /* Selected state - Gradio adds 'selected' class to the label in some versions, or we check input:checked */ .sidebar-nav label.selected { border-color: rgba(99,102,241,0.35) !important; box-shadow: inset 0 0 0 1px rgba(99,102,241,0.25), 0 1px 2px rgba(0,0,0,0.25) !important; background: linear-gradient(180deg, rgba(99,102,241,0.18), rgba(59,130,246,0.14)) !important; color: rgba(255, 255, 255, 0.95) !important; } /* Light theme adjustments for sidebar */ @media (prefers-color-scheme: light) { .sidebar-nav label { border-color: rgba(0, 0, 0, 0.08) !important; background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,0.90)) !important; color: rgba(0, 0, 0, 0.85) !important; } .sidebar-nav label:hover { border-color: rgba(99,102,241,0.25) !important; background: linear-gradient(180deg, rgba(99,102,241,0.08), rgba(59,130,246,0.06)) !important; } .sidebar-nav label.selected { border-color: rgba(99,102,241,0.35) !important; background: linear-gradient(180deg, rgba(99,102,241,0.16), rgba(59,130,246,0.12)) !important; color: rgba(0, 0, 0, 0.85) !important; } } /* Hide scrollbars/arrows that can appear on the description block in some browsers */ article.prose, .prose, .gr-prose { overflow: visible !important; max-height: none !important; -ms-overflow-style: none !important; /* IE/Edge */ scrollbar-width: none !important; /* Firefox */ } article.prose::-webkit-scrollbar, .prose::-webkit-scrollbar, .gr-prose::-webkit-scrollbar { display: none !important; /* Chrome/Safari */ } /* Fix for white background on single-line inputs in dark mode */ .gradio-container input[type="text"], .gradio-container input[type="password"], .gradio-container input[type="number"], .gradio-container input[type="email"] { background-color: var(--input-background-fill) !important; color: var(--body-text-color) !important; } /* Custom glossy purple styling for primary action buttons */ .gradio-container button.primary { border: 1px solid rgba(99, 102, 241, 0.35) !important; background: linear-gradient(180deg, rgba(99, 102, 241, 0.25), rgba(59, 130, 246, 0.20)) !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 2px 4px rgba(0, 0, 0, 0.15) !important; color: rgba(255, 255, 255, 0.95) !important; transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .06s ease !important; } .gradio-container button.primary:hover { border-color: rgba(99, 102, 241, 0.5) !important; background: linear-gradient(180deg, rgba(99, 102, 241, 0.35), rgba(59, 130, 246, 0.28)) !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 3px 6px rgba(0, 0, 0, 0.2) !important; } .gradio-container button.primary:active { transform: scale(0.98) !important; box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2), 0 1px 2px rgba(0, 0, 0, 0.1) !important; } @media (prefers-color-scheme: light) { .gradio-container button.primary { border-color: rgba(99, 102, 241, 0.4) !important; background: linear-gradient(180deg, rgba(99, 102, 241, 0.85), rgba(79, 70, 229, 0.75)) !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 2px 4px rgba(0, 0, 0, 0.12) !important; color: rgba(255, 255, 255, 0.98) !important; } .gradio-container button.primary:hover { background: linear-gradient(180deg, rgba(99, 102, 241, 0.95), rgba(79, 70, 229, 0.85)) !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 3px 6px rgba(0, 0, 0, 0.15) !important; } } /* Hide the actual tabs since we use the sidebar to control them */ .hidden-tabs .tab-nav, .hidden-tabs [role="tablist"] { display: none !important; } /* Hide the entire first row of the tabs container (contains tab buttons + overflow) */ .hidden-tabs > div:first-child { display: none !important; } /* Ensure audio component buttons remain visible - they're inside tab panels, not the first row */ .hidden-tabs [role="tabpanel"] button { display: inline-flex !important; } /* Custom scrollbar styling */ * { scrollbar-width: thin; scrollbar-color: rgba(61, 212, 159, 0.4) rgba(255, 255, 255, 0.05); } *::-webkit-scrollbar { width: 8px; height: 8px; } *::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 4px; } *::-webkit-scrollbar-thumb { background: linear-gradient(180deg, rgba(61, 212, 159, 0.5), rgba(17, 186, 136, 0.4)); border-radius: 4px; border: 1px solid rgba(119, 247, 209, 0.2); } *::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, rgba(85, 250, 192, 0.7), rgba(65, 184, 131, 0.6)); } *::-webkit-scrollbar-corner { background: rgba(255, 255, 255, 0.05); } @media (prefers-color-scheme: light) { * { scrollbar-color: rgba(61, 212, 159, 0.4) rgba(0, 0, 0, 0.05); } *::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.05); } *::-webkit-scrollbar-thumb { background: linear-gradient(180deg, rgba(61, 212, 159, 0.5), rgba(17, 186, 136, 0.4)); border-color: rgba(0, 0, 0, 0.1); } *::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, rgba(85, 250, 192, 0.7), rgba(65, 184, 131, 0.6)); } *::-webkit-scrollbar-corner { background: rgba(0, 0, 0, 0.05); } } """ # Build each tab interface using modular builders fetch_interface = build_fetch_interface() web_search_interface = build_search_interface() agent_terminal_interface = build_agent_terminal_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 = [ agent_terminal_interface, fetch_interface, web_search_interface, code_interface, shell_interface, fs_interface, obsidian_interface, memory_interface, kokoro_interface, image_generation_interface, video_generation_interface, deep_research_interface, ] _tab_names = [ "Agent Terminal", "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") as demo: with gr.Sidebar(width=300, elem_classes="app-sidebar"): gr.Markdown("## Nymbo/Tools MCP\n

General purpose tools useful for any agent.

\nhttps://nymbo.net/gradio_api/mcp/") with gr.Accordion("Information", open=False): gr.HTML( """

Connecting from an MCP Client

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

Example client configuration:

{
  "mcpServers": {
    "nymbo-tools": {
      "url": "https://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

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.

Tool Notes & Kokoro Voice Legend

No authentication required for:

  • Web_Fetch
  • Web_Search
  • Agent_Terminal
  • Code_Interpreter
  • Memory_Manager
  • Generate_Speech
  • File_System
  • Shell_Command

Kokoro voice prefixes

Accent Female Male
American af am
British bf bm
European ef em
French ff
Hindi hf hm
Italian if im
Japanese jf jm
Portuguese pf pm
Chinese zf zm
""" ) gr.Markdown("### Tools") tool_selector = gr.Radio( choices=_tab_names, value=_tab_names[0], label="Select Tool", show_label=False, container=False, elem_classes="sidebar-nav" ) with gr.Tabs(elem_classes="hidden-tabs", selected=_tab_names[0]) as tool_tabs: for name, interface in zip(_tab_names, _interfaces): with gr.TabItem(label=name, id=name, elem_id=f"tab-{name}"): interface.render() # Use JavaScript to click the hidden tab button when the radio selection changes tool_selector.change( fn=None, inputs=tool_selector, outputs=None, js="(selected_tool) => { const buttons = document.querySelectorAll('.hidden-tabs button'); buttons.forEach(btn => { if (btn.innerText.trim() === selected_tool) { btn.click(); } }); }" ) if __name__ == "__main__": demo.launch(mcp_server=True, theme="Nymbo/Nymbo_Theme", css=CSS_STYLES, ssr_mode=False)