mcp-demo / app.py
DealerMax's picture
Initial: DealerMax MCP demo Gradio Space — interactive vetrina for 6 MCP tools
ca002c4 verified
"""
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()