""" DealerMax MCP demo — Gradio Space. Vetrina interattiva del Model Context Protocol server pubblico di DealerMax (registrato sull'Anthropic MCP Registry come `app.dealermax/public-search`). Espone i 6 tool MCP via UI semplice per chi non vuole / non puo' configurare un client MCP nativo (Claude Desktop, ChatGPT, ecc.). Endpoint upstream: https://mcp.dealermax.app/mcp/ Trasporto: streamable-http JSON-RPC 2.0 Tool: search_vehicles, search_nlt_offers, get_vehicle_details, find_dealer, get_market_intel, get_vehicle_specs Cosa fa la Space: - radio button per selezionare il tool - form per input parametri (cambia in base al tool) - bottoni "esempi pronti" che pre-popolano i campi - chiamata HTTP POST diretta al MCP server (no SDK, no auth) - rendering risultato JSON pretty-print + tabella highlights Niente API key richiesta. Rate limit equo lato server (60 req/min/IP). """ from __future__ import annotations import json from typing import Any import gradio as gr import requests MCP_ENDPOINT = "https://mcp.dealermax.app/mcp/" HEADERS = { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", } def call_mcp_tool(tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]: """Chiama un tool MCP via JSON-RPC 2.0. Restituisce il payload risultato parsato come dict (gli MCP tool DealerMax tornano sempre JSON serializzato in `content[0].text`). """ arguments = {k: v for k, v in arguments.items() if v not in (None, "", 0)} payload = { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": tool_name, "arguments": arguments}, } try: r = requests.post(MCP_ENDPOINT, json=payload, headers=HEADERS, timeout=15) r.raise_for_status() data = r.json() except requests.exceptions.RequestException as e: return {"error": f"Network error: {e}"} except json.JSONDecodeError as e: return {"error": f"Invalid JSON response: {e}"} if "error" in data: return {"error": data["error"]} result = data.get("result", {}) content = result.get("content", []) if content and isinstance(content, list): first = content[0] if first.get("type") == "text": try: return json.loads(first["text"]) except json.JSONDecodeError: return {"raw_text": first["text"]} return result def format_output(payload: dict[str, Any]) -> str: """Pretty-print JSON con indent.""" return json.dumps(payload, indent=2, ensure_ascii=False) # --------------------------------------------------------------------------- # # Tool wrappers (1 funzione per tool, parametri allineati allo schema MCP) # --------------------------------------------------------------------------- # def tool_search_vehicles(query: str, region: str, brand: str, fuel_type: str, budget_max: int, limit: int) -> str: return format_output(call_mcp_tool("search_vehicles", { "query": query, "region": region or None, "brand": brand or None, "fuel_type": fuel_type or None, "budget_max": budget_max or None, "limit": limit or 5, })) def tool_search_nlt_offers(query: str, durata_max_mesi: int, canone_max: int, region: str, limit: int) -> str: return format_output(call_mcp_tool("search_nlt_offers", { "query": query, "durata_max_mesi": durata_max_mesi or None, "canone_max": canone_max or None, "region": region or None, "limit": limit or 5, })) def tool_get_vehicle_details(vehicle_slug: str) -> str: return format_output(call_mcp_tool("get_vehicle_details", {"vehicle_slug": vehicle_slug})) def tool_find_dealer(region: str, brand: str) -> str: return format_output(call_mcp_tool("find_dealer", { "region": region or None, "brand": brand or None, })) def tool_get_market_intel(query: str, types: str, limit: int) -> str: types_list = [t.strip() for t in types.split(",") if t.strip()] if types else None return format_output(call_mcp_tool("get_market_intel", { "query": query, "types": types_list, "limit": limit or 5, })) def tool_get_vehicle_specs(query: str, brand: str, model: str, fuel_type: str, limit: int) -> str: return format_output(call_mcp_tool("get_vehicle_specs", { "query": query or None, "brand": brand or None, "model": model or None, "fuel_type": fuel_type or None, "limit": limit or 5, })) # --------------------------------------------------------------------------- # # UI Gradio # --------------------------------------------------------------------------- # INTRO_MD = """ # DealerMax MCP — vetrina interattiva Demo pubblica del **Model Context Protocol server di DealerMax**, registrato sul [registro ufficiale Anthropic](https://registry.modelcontextprotocol.io/v0.1/servers?search=app.dealermax) come `app.dealermax/public-search` v1.0.0. Sei strumenti su un solo endpoint pubblico, no-auth, fair-use rate-limited: | Tool | Cosa restituisce | |---|---| | `search_vehicles` | Veicoli usati cross-dealer del network DealerMax | | `search_nlt_offers` | Offerte di noleggio lungo termine (NLT) cross-dealer | | `get_vehicle_details` | Dettaglio singolo veicolo (specs, prezzo, dealer, podcast) | | `find_dealer` | Directory concessionari per regione / brand | | `get_market_intel` | Knowledge base automotive italiana (guide, glossario, FAQ, news) | | `get_vehicle_specs` | Specifiche tecniche pubbliche di veicoli (dimensioni, consumi, motore...) | Endpoint: `https://mcp.dealermax.app/mcp/` · Rate limit: 60 req/min · License: CC-BY-4.0 """ with gr.Blocks(title="DealerMax MCP demo", theme=gr.themes.Soft()) as demo: gr.Markdown(INTRO_MD) with gr.Tabs(): # --- search_vehicles --- with gr.Tab("🚗 search_vehicles"): gr.Markdown("Ricerca cross-dealer di veicoli usati italiani. Filtri opzionali su regione, brand, alimentazione, budget.") with gr.Row(): sv_query = gr.Textbox(label="Query semantica", placeholder="es. SUV ibrido familiare", value="SUV ibrido familiare") sv_region = gr.Textbox(label="Regione/provincia (opzionale)", placeholder="Lombardia, MI, Milano…") with gr.Row(): sv_brand = gr.Textbox(label="Brand (opzionale)", placeholder="BMW, Toyota…") sv_fuel = gr.Textbox(label="Alimentazione (opzionale)", placeholder="benzina, ibrido, elettrico…") sv_budget = gr.Number(label="Budget max € (opzionale)", value=None, precision=0) sv_limit = gr.Number(label="Limit", value=5, precision=0, minimum=1, maximum=30) sv_btn = gr.Button("Cerca veicoli", variant="primary") sv_out = gr.Code(language="json", label="Risultato JSON") sv_btn.click(tool_search_vehicles, [sv_query, sv_region, sv_brand, sv_fuel, sv_budget, sv_limit], sv_out) # --- search_nlt_offers --- with gr.Tab("📅 search_nlt_offers"): gr.Markdown("Offerte di noleggio lungo termine cross-dealer. Filtri su durata massima, canone massimo, regione.") with gr.Row(): nlt_query = gr.Textbox(label="Query", placeholder="es. elettrica city car under 300/mese", value="elettrica city car") nlt_durata = gr.Number(label="Durata max mesi (36/48/60)", value=None, precision=0) with gr.Row(): nlt_canone = gr.Number(label="Canone max €/mese", value=None, precision=0) nlt_region = gr.Textbox(label="Regione/provincia", placeholder="Lazio, Roma…") nlt_limit = gr.Number(label="Limit", value=5, precision=0, minimum=1, maximum=30) nlt_btn = gr.Button("Cerca offerte NLT", variant="primary") nlt_out = gr.Code(language="json", label="Risultato JSON") nlt_btn.click(tool_search_nlt_offers, [nlt_query, nlt_durata, nlt_canone, nlt_region, nlt_limit], nlt_out) # --- get_vehicle_specs --- with gr.Tab("📐 get_vehicle_specs"): gr.Markdown( "Specifiche tecniche pubbliche dei veicoli del mercato italiano. Risponde a query come " "*Quanto è lunga la Mazda 3?*, *Quanto consuma una Peugeot 2008 ibrida?*" ) with gr.Row(): vs_query = gr.Textbox(label="Query free-text (opzionale)", placeholder="Mazda 3 2024", value="") vs_brand = gr.Textbox(label="Marca", placeholder="Peugeot, Mazda, Tesla…", value="Peugeot") with gr.Row(): vs_model = gr.Textbox(label="Modello", placeholder="2008, Model 3, 3…", value="2008") vs_fuel = gr.Textbox(label="Alimentazione (opzionale)", placeholder="ibrido, elettrico…", value="ibrido") vs_limit = gr.Number(label="Limit", value=3, precision=0, minimum=1, maximum=30) vs_btn = gr.Button("Cerca specifiche", variant="primary") vs_out = gr.Code(language="json", label="Risultato JSON") vs_btn.click(tool_get_vehicle_specs, [vs_query, vs_brand, vs_model, vs_fuel, vs_limit], vs_out) # --- get_vehicle_details --- with gr.Tab("🔍 get_vehicle_details"): gr.Markdown( "Dettaglio completo di un singolo veicolo (specs, prezzo, dealer, podcast). " "Accetta UUID o slug `marca-modello-id_auto`." ) vd_slug = gr.Textbox(label="Vehicle slug / UUID", placeholder="UUID o slug del veicolo") vd_btn = gr.Button("Recupera dettaglio", variant="primary") vd_out = gr.Code(language="json", label="Risultato JSON") vd_btn.click(tool_get_vehicle_details, [vd_slug], vd_out) # --- find_dealer --- with gr.Tab("🏬 find_dealer"): gr.Markdown("Directory dei dealer attivi nel network DealerMax. Filtra per regione/provincia/citta + brand venduto.") with gr.Row(): fd_region = gr.Textbox(label="Regione/provincia (opzionale)", placeholder="Toscana, FI, Firenze…", value="Lombardia") fd_brand = gr.Textbox(label="Brand (opzionale)", placeholder="BMW, Tesla…") fd_btn = gr.Button("Cerca dealer", variant="primary") fd_out = gr.Code(language="json", label="Risultato JSON") fd_btn.click(tool_find_dealer, [fd_region, fd_brand], fd_out) # --- get_market_intel --- with gr.Tab("📚 get_market_intel"): gr.Markdown( "Knowledge base automotive italiana: guide editoriali, glossario (212 termini), FAQ globali (139), news. " "Filtra per tipi `guide,glossary,faq,news` (default tutti)." ) with gr.Row(): mi_query = gr.Textbox(label="Query semantica", placeholder="es. incentivi auto elettriche 2026", value="incentivi auto elettriche 2026") mi_types = gr.Textbox(label="Tipi (comma-separated, opzionale)", placeholder="guide,glossary,faq,news") mi_limit = gr.Number(label="Limit", value=5, precision=0, minimum=1, maximum=30) mi_btn = gr.Button("Cerca knowledge", variant="primary") mi_out = gr.Code(language="json", label="Risultato JSON") mi_btn.click(tool_get_market_intel, [mi_query, mi_types, mi_limit], mi_out) gr.Markdown( "## Configura il tuo client AI nativo\n\n" "Per usare il server MCP direttamente da Claude Desktop, ChatGPT, Cursor, Gemini-CLI, " "vai sulla pagina onboarding ufficiale: [dealermax.app/mcp](https://dealermax.app/mcp).\n\n" "Repository: [VMAzure/dealermax-mcp](https://github.com/VMAzure/dealermax-mcp) · " "Operatore: AZURE Srl (P.IVA IT13005450963) · Contatto: support@dealermax.app" ) if __name__ == "__main__": demo.launch()