| """ |
| 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) |
|
|
|
|
| |
| |
| |
| 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, |
| })) |
|
|
|
|
| |
| |
| |
| 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(): |
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|