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.Deep_Research import build_interface as build_research_interface from Modules.File_System import build_interface as build_fs_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_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; } } /* 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; } """ # 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() deep_research_interface = build_research_interface() fs_interface = build_fs_interface() shell_interface = build_shell_interface() _interfaces = [ fetch_interface, concise_interface, code_interface, shell_interface, fs_interface, memory_interface, deep_research_interface, ] _tab_names = [ "Web Fetch", "Web Search", "Code Interpreter", "Shell Command", "File System", "Memory Manager", "Deep Research", ] with gr.Blocks(title="Nymbo/Tools MCP") 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 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
🛠️

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

""" ) gr.TabbedInterface(interface_list=_interfaces, tab_names=_tab_names) if __name__ == "__main__": demo.launch(mcp_server=True, theme="Nymbo/Nymbo_Theme", css=CSS_STYLES)