File size: 11,887 Bytes
ca002c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
"""
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()