claracars-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "claracars",
3
+ "description": "Search used & imported cars in Portugal, estimate car import tax (ISV), estimate resale value, and reach Clara Carros.",
4
+ "version": "0.1.0",
5
+ "vendor": "Clara Carros",
6
+ "homepage": "https://claracars.pt",
7
+ "documentation": "https://github.com/claracarspt/mcp",
8
+ "license": "MIT",
9
+ "transports": {
10
+ "streamable_http": { "url": "https://claracars.pt/mcp" },
11
+ "stdio": { "command": "npx", "args": ["-y", "claracars-mcp"] }
12
+ },
13
+ "tools": [
14
+ "search_inventory",
15
+ "get_car",
16
+ "calculate_isv",
17
+ "calculate_iuc",
18
+ "estimate_resale_value",
19
+ "list_services",
20
+ "contact_me",
21
+ "request_import_quote"
22
+ ]
23
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clara Carros
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Clara Carros MCP server
2
+
3
+ [![MCP](https://img.shields.io/badge/MCP-server-blue)](https://modelcontextprotocol.io) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
4
+
5
+ An [MCP](https://modelcontextprotocol.io) server that lets AI assistants work with **[Clara Carros](https://claracars.pt)** — used & imported cars in Portugal. Search live stock, estimate the Portuguese import tax (**ISV**), estimate a car's resale value, and get in touch.
6
+
7
+ It's a thin, open-source wrapper over the public `claracars.pt` API — no keys, no setup beyond adding the server.
8
+
9
+ ## Tools
10
+
11
+ | Tool | What it does |
12
+ |------|--------------|
13
+ | `search_inventory` | Search current stock by make, model, fuel, price, year |
14
+ | `get_car` | Full details + specs for one car |
15
+ | `calculate_isv` | Estimate Portuguese car import tax (ISV) |
16
+ | `calculate_iuc` | Estimate Portuguese annual road tax (IUC) |
17
+ | `estimate_resale_value` | Market value range for a car in Portugal |
18
+ | `list_services` | What Clara Carros offers |
19
+ | `contact_me` | Ask a human to get in touch (needs the user's consent + contact) |
20
+ | `request_import_quote` | Ask Clara Carros to source & quote a car to import |
21
+
22
+ ## Use it
23
+
24
+ ### Remote (add a URL)
25
+
26
+ ```
27
+ https://claracars.pt/mcp
28
+ ```
29
+
30
+ Add it in any client that supports remote/Streamable HTTP MCP servers.
31
+
32
+ ### Local (stdio, via npx)
33
+
34
+ Claude Desktop / Cline / Continue config:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "claracars": {
40
+ "command": "npx",
41
+ "args": ["-y", "claracars-mcp"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Examples
48
+
49
+ - "Find diesel wagons under €20,000 in Clara Carros stock."
50
+ - "What's the ISV and annual IUC road tax on a 2016 diesel, 1950 cc, 130 g/km CO₂?"
51
+ - "What's my 2018 VW Golf, 100,000 km worth in Portugal?"
52
+ - "Ask Clara Carros to source a BMW 320d Touring 2021, my name is … and phone …"
53
+
54
+ ## Configuration
55
+
56
+ | Env var | Default | Purpose |
57
+ |---------|---------|---------|
58
+ | `CLARACARS_API_BASE` | `https://claracars.pt/api/public` | Public API base |
59
+ | `CLARACARS_LANG` | `en` | Response language (pt/en/ru/ua) |
60
+ | `PORT` / `MCP_PATH` | `8099` / `/mcp` | HTTP server (remote mode) |
61
+
62
+ ## Develop
63
+
64
+ ```bash
65
+ npm install
66
+ npm run build
67
+ node dist/index.js # stdio
68
+ npm run start:http # remote HTTP on :8099/mcp
69
+ ```
70
+
71
+ ## Notes
72
+
73
+ - **Estimates, not quotes.** ISV and resale figures are estimates; the official ISV is assessed by the Portuguese tax authority.
74
+ - **Privacy.** `contact_me` / `request_import_quote` send the details the user provides to Clara Carros ([privacy policy](https://claracars.pt/en/privacy)); they're rate-limited and require explicit user intent.
75
+ - No business logic or data lives in this repo — it only calls the public API.
76
+
77
+ MIT © Clara Carros
package/dist/api.js ADDED
@@ -0,0 +1,45 @@
1
+ // Thin client for the public claracars.pt API. No secrets, no business logic here —
2
+ // everything (inventory, valuation, ISV, leads) lives behind these public endpoints.
3
+ export const API_BASE = process.env.CLARACARS_API_BASE?.replace(/\/$/, "") ||
4
+ "https://claracars.pt/api/public";
5
+ export const SITE = process.env.CLARACARS_SITE || "claracars";
6
+ export const DEFAULT_LANG = process.env.CLARACARS_LANG || "en";
7
+ const UA = "claracars-mcp/0.1";
8
+ async function req(path, init) {
9
+ const url = path.startsWith("http") ? path : `${API_BASE}${path}`;
10
+ const res = await fetch(url, {
11
+ ...init,
12
+ headers: { "user-agent": UA, accept: "application/json", ...(init?.headers || {}) },
13
+ });
14
+ const text = await res.text();
15
+ let data = null;
16
+ try {
17
+ data = text ? JSON.parse(text) : null;
18
+ }
19
+ catch {
20
+ data = { raw: text };
21
+ }
22
+ if (!res.ok) {
23
+ const detail = data?.detail || data?.raw || res.statusText;
24
+ throw new Error(`API ${res.status}: ${detail}`);
25
+ }
26
+ return data;
27
+ }
28
+ export function apiGet(path, params) {
29
+ const qs = new URLSearchParams();
30
+ if (params) {
31
+ for (const [k, v] of Object.entries(params)) {
32
+ if (v !== undefined && v !== null && v !== "")
33
+ qs.set(k, String(v));
34
+ }
35
+ }
36
+ const sep = path.includes("?") ? "&" : "?";
37
+ return req(qs.toString() ? `${path}${sep}${qs}` : path);
38
+ }
39
+ export function apiPost(path, body) {
40
+ return req(path, {
41
+ method: "POST",
42
+ headers: { "content-type": "application/json" },
43
+ body: JSON.stringify(body),
44
+ });
45
+ }
package/dist/http.js ADDED
@@ -0,0 +1,68 @@
1
+ // Clara Carros MCP server — remote Streamable HTTP transport (for "add a URL" clients).
2
+ // Stateless: a fresh server+transport per request (simple, horizontally scalable).
3
+ import { createServer } from "node:http";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
+ import { registerTools } from "./tools.js";
7
+ const PORT = Number(process.env.PORT || 8099);
8
+ const HOST = process.env.MCP_HOST || "127.0.0.1"; // bind loopback; front with an (auth) proxy, never all-interfaces
9
+ const MCP_PATH = process.env.MCP_PATH || "/mcp";
10
+ // Anti-DNS-rebinding: this SDK's StreamableHTTPServerTransport doesn't expose allowedHosts, so gate here.
11
+ const ALLOWED_HOSTS = (process.env.MCP_ALLOWED_HOSTS || "claracars.pt,mcp.claracars.pt,localhost:8099,127.0.0.1:8099")
12
+ .split(",").map((s) => s.trim().toLowerCase());
13
+ const ALLOWED_ORIGINS = (process.env.MCP_ALLOWED_ORIGINS || "https://claracars.pt")
14
+ .split(",").map((s) => s.trim());
15
+ function originAllowed(req) {
16
+ const host = String(req.headers.host || "").toLowerCase();
17
+ if (!ALLOWED_HOSTS.includes(host))
18
+ return false; // blocks rebinding to attacker Host
19
+ const origin = req.headers.origin; // native MCP clients send none → allowed
20
+ if (origin && !ALLOWED_ORIGINS.includes(origin))
21
+ return false; // blocks browser cross-origin
22
+ return true;
23
+ }
24
+ function readBody(req) {
25
+ return new Promise((resolve) => {
26
+ let raw = "";
27
+ req.on("data", (c) => (raw += c));
28
+ req.on("end", () => {
29
+ try {
30
+ resolve(raw ? JSON.parse(raw) : undefined);
31
+ }
32
+ catch {
33
+ resolve(undefined);
34
+ }
35
+ });
36
+ });
37
+ }
38
+ async function handleMcp(req, res) {
39
+ const server = new McpServer({ name: "claracars", version: "0.1.0" });
40
+ registerTools(server);
41
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
42
+ res.on("close", () => {
43
+ transport.close();
44
+ server.close();
45
+ });
46
+ await server.connect(transport);
47
+ await transport.handleRequest(req, res, await readBody(req));
48
+ }
49
+ createServer((req, res) => {
50
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
51
+ if (url.pathname === "/healthz") {
52
+ res.writeHead(200, { "content-type": "text/plain" }).end("ok");
53
+ return;
54
+ }
55
+ if (url.pathname === MCP_PATH) {
56
+ if (!originAllowed(req)) {
57
+ res.writeHead(403, { "content-type": "text/plain" }).end("forbidden");
58
+ return;
59
+ }
60
+ handleMcp(req, res).catch((e) => {
61
+ console.error("mcp error", e);
62
+ if (!res.headersSent)
63
+ res.writeHead(500).end();
64
+ });
65
+ return;
66
+ }
67
+ res.writeHead(404).end();
68
+ }).listen(PORT, HOST, () => console.error(`claracars-mcp HTTP on ${HOST}:${PORT}${MCP_PATH}`));
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Clara Carros MCP server — stdio transport (for `npx claracars-mcp`, Claude Desktop, Cline, etc.)
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { registerTools } from "./tools.js";
6
+ const server = new McpServer({ name: "claracars", version: "0.1.0" });
7
+ registerTools(server);
8
+ const transport = new StdioServerTransport();
9
+ await server.connect(transport);
10
+ // stderr only — stdout is the MCP channel
11
+ console.error("claracars-mcp running on stdio");
package/dist/tools.js ADDED
@@ -0,0 +1,176 @@
1
+ import { z } from "zod";
2
+ import { apiGet, apiPost, SITE, DEFAULT_LANG } from "./api.js";
3
+ const SITE_URL = "https://claracars.pt";
4
+ const text = (s) => ({ content: [{ type: "text", text: s }] });
5
+ const eur = (n) => (typeof n === "number" ? `€${n.toLocaleString("en-US")}` : "—");
6
+ const SERVICES = [
7
+ { id: "stock", name: "Cars in stock", desc: "Curated used cars ready to view in Portugal.", url: `${SITE_URL}/en/carros` },
8
+ { id: "import", name: "Import to order", desc: "Source & import a specific car from German/EU auctions, legalized and delivered.", url: `${SITE_URL}/en/import` },
9
+ { id: "sourcing", name: "Car sourcing", desc: "We find the best option for you in Portugal or abroad.", url: `${SITE_URL}/en/sourcing` },
10
+ { id: "auction", name: "Auction buying", desc: "We bid for you at professional auctions (auto1 etc.).", url: `${SITE_URL}/en/auction` },
11
+ { id: "sell", name: "We buy your car", desc: "Free valuation, fast payout — foreign plates welcome.", url: `${SITE_URL}/en/sell` },
12
+ { id: "isv", name: "ISV calculator", desc: "Portuguese car import tax estimate.", url: `${SITE_URL}/en/isv` },
13
+ ];
14
+ export function registerTools(server) {
15
+ // ---- search_inventory ---------------------------------------------------
16
+ server.tool("search_inventory", "Search Clara Carros' current stock of used cars in Portugal. Filter by make, model, fuel, max price and min year. Returns matching cars with price, year, mileage and a link.", {
17
+ make: z.string().optional().describe("Brand, e.g. 'Mercedes-Benz', 'Volkswagen'"),
18
+ model: z.string().optional().describe("Model name, e.g. 'Golf'"),
19
+ fuel: z.string().optional().describe("Fuel: petrol, diesel, hybrid, phev, electric, lpg"),
20
+ max_price: z.number().optional().describe("Maximum price in EUR"),
21
+ min_year: z.number().optional().describe("Earliest registration year"),
22
+ limit: z.number().int().min(1).max(50).optional().describe("Max results (default 20)"),
23
+ }, async (a) => {
24
+ const data = await apiGet("/cars", { site: SITE, lang: DEFAULT_LANG });
25
+ let cars = data?.cars || [];
26
+ const norm = (s) => String(s || "").toLowerCase();
27
+ if (a.make)
28
+ cars = cars.filter((c) => norm(c.make).includes(norm(a.make)));
29
+ if (a.model)
30
+ cars = cars.filter((c) => norm(c.model).includes(norm(a.model)));
31
+ if (a.fuel)
32
+ cars = cars.filter((c) => norm(c.fuel).includes(norm(a.fuel)) || norm(c.fuel_code).includes(norm(a.fuel)));
33
+ if (typeof a.max_price === "number")
34
+ cars = cars.filter((c) => (c.price_eur ?? Infinity) <= a.max_price);
35
+ if (typeof a.min_year === "number")
36
+ cars = cars.filter((c) => (c.year ?? 0) >= a.min_year);
37
+ cars = cars.slice(0, a.limit ?? 20);
38
+ if (!cars.length)
39
+ return text("No cars in stock match those filters right now. Try widening the search, or use request_import_quote to have Clara Carros source one.");
40
+ const lines = cars.map((c) => `• ${c.make} ${c.model} (${c.year || "—"}) — ${eur(c.price_eur)}, ${(c.km ?? 0).toLocaleString("en-US")} km, ${c.fuel || "—"}\n ${SITE_URL}/${DEFAULT_LANG}/car/${c.slug}`);
41
+ return text(`${cars.length} car(s) in stock:\n\n${lines.join("\n")}`);
42
+ });
43
+ // ---- get_car ------------------------------------------------------------
44
+ server.tool("get_car", "Get full details for one car by its slug (from search_inventory links, e.g. 'mercedes-benz-e-klasse-2016').", { slug: z.string().describe("Car slug from a search_inventory result URL") }, async (a) => {
45
+ const c = await apiGet(`/cars/${encodeURIComponent(a.slug)}`, { site: SITE, lang: DEFAULT_LANG });
46
+ const specs = [
47
+ `Price: ${eur(c.price_eur)}`,
48
+ `Year: ${c.year || "—"}`,
49
+ `Mileage: ${(c.km ?? 0).toLocaleString("en-US")} km`,
50
+ `Fuel: ${c.fuel || "—"}`,
51
+ `Gearbox: ${c.gearbox || "—"}`,
52
+ `Body: ${c.body || "—"}`,
53
+ c.hp ? `Power: ${c.hp} hp` : null,
54
+ c.cc ? `Engine: ${c.cc} cc` : null,
55
+ c.co2 ? `CO₂: ${c.co2} g/km` : null,
56
+ c.colour ? `Colour: ${c.colour}` : null,
57
+ ].filter(Boolean).join("\n");
58
+ const desc = c.description ? `\n\n${c.description}` : "";
59
+ return text(`${c.make} ${c.model} (${c.year || "—"})\n${specs}${desc}\n\n${SITE_URL}/${DEFAULT_LANG}/car/${c.slug}`);
60
+ });
61
+ // ---- calculate_isv ------------------------------------------------------
62
+ server.tool("calculate_isv", "Estimate Portuguese car import tax (ISV, Imposto Sobre Veículos) for a vehicle. Needs engine displacement (cc), CO₂ (g/km), fuel and registration year. This is an estimate, not an official assessment.", {
63
+ cc: z.number().int().describe("Engine displacement in cm³, e.g. 1950"),
64
+ co2: z.number().describe("CO₂ emissions in g/km (WLTP), e.g. 130"),
65
+ fuel: z.string().describe("Fuel: petrol, diesel, hybrid, phev, electric"),
66
+ year: z.number().int().describe("First registration year, e.g. 2016"),
67
+ used: z.boolean().optional().describe("Used vehicle (true, default) or new (false)"),
68
+ }, async (a) => {
69
+ const r = await apiGet("/isv", {
70
+ cc: a.cc, co2: a.co2, fuel: a.fuel, year: a.year,
71
+ used: a.used === false ? 0 : 1,
72
+ });
73
+ const isv = r?.isv || r;
74
+ if (isv?.exempt)
75
+ return text(`Estimated ISV: €0 (exempt). ${isv.note || ""}\n\nEstimate only — confirm at ${SITE_URL}/${DEFAULT_LANG}/isv`);
76
+ const b = isv?.breakdown || {};
77
+ const parts = [
78
+ `Estimated ISV: ${eur(isv?.isv)}`,
79
+ isv?.table ? `Table: ${isv.table}` : null,
80
+ typeof isv?.comp_cilindrada === "number" ? ` engine component: ${eur(isv.comp_cilindrada)}` : null,
81
+ typeof isv?.comp_ambiental === "number" ? ` environmental component: ${eur(isv.comp_ambiental)}` : null,
82
+ typeof isv?.diesel_surcharge === "number" && isv.diesel_surcharge ? ` diesel surcharge: ${eur(isv.diesel_surcharge)}` : null,
83
+ isv?.note ? isv.note : null,
84
+ ].filter(Boolean);
85
+ return text(`${parts.join("\n")}\n\nEstimate only — full breakdown at ${SITE_URL}/${DEFAULT_LANG}/isv`);
86
+ });
87
+ // ---- calculate_iuc ------------------------------------------------------
88
+ server.tool("calculate_iuc", "Estimate the Portuguese annual road tax (IUC, Imposto Único de Circulação) for a car — the yearly tax an owner pays. Needs engine displacement (cc), CO₂ (g/km), fuel and registration year. Estimate, not an official assessment.", {
89
+ cc: z.number().int().describe("Engine displacement in cm³, e.g. 1950"),
90
+ co2: z.number().describe("CO₂ emissions in g/km, e.g. 130"),
91
+ fuel: z.string().describe("Fuel: petrol, diesel, hybrid, phev, electric"),
92
+ year: z.number().int().describe("First registration year, e.g. 2016"),
93
+ }, async (a) => {
94
+ const r = await apiGet("/isv", { cc: a.cc, co2: a.co2, fuel: a.fuel, year: a.year });
95
+ const iuc = r?.iuc;
96
+ if (!iuc)
97
+ return text(`Couldn't compute IUC for those inputs. Try the calculator at ${SITE_URL}/${DEFAULT_LANG}/iuc`);
98
+ if (iuc.exempt)
99
+ return text(`Estimated IUC: €0/year (exempt). ${iuc.note || ""}\n\nEstimate only — ${SITE_URL}/${DEFAULT_LANG}/iuc`);
100
+ const parts = [
101
+ `Estimated IUC (annual road tax): ${eur(iuc.iuc)}/year`,
102
+ iuc.category ? `Category: ${iuc.category}` : null,
103
+ typeof iuc.taxa_cilindrada === "number" ? ` engine component: ${eur(iuc.taxa_cilindrada)}` : null,
104
+ typeof iuc.taxa_co2 === "number" ? ` CO₂ component: ${eur(iuc.taxa_co2)}` : null,
105
+ typeof iuc.diesel_adic === "number" && iuc.diesel_adic ? ` diesel surcharge: ${eur(iuc.diesel_adic)}` : null,
106
+ iuc.note ? iuc.note : null,
107
+ ].filter(Boolean);
108
+ return text(`${parts.join("\n")}\n\nEstimate only — full breakdown at ${SITE_URL}/${DEFAULT_LANG}/iuc`);
109
+ });
110
+ // ---- estimate_resale_value ---------------------------------------------
111
+ server.tool("estimate_resale_value", "Estimate what a car is worth on the Portuguese market (the range comparable cars are listed for), based on real comparable listings. Needs make, model, year, mileage and fuel. Clara Carros also BUYS cars directly — after giving the estimate, offer to connect the user for a firm buy-out offer (via contact_me or the /sell page).", {
112
+ make: z.string().describe("Brand, e.g. 'Volkswagen'"),
113
+ model: z.string().describe("Model, e.g. 'Golf'"),
114
+ year: z.number().int().describe("Registration year"),
115
+ km: z.number().int().describe("Mileage in km"),
116
+ fuel: z.string().optional().describe("Fuel: petrol, diesel, hybrid, electric"),
117
+ }, async (a) => {
118
+ const v = await apiGet("/valuation", { make: a.make, model: a.model, year: a.year, km: a.km, fuel: a.fuel });
119
+ const sellUrl = `${SITE_URL}/${DEFAULT_LANG}/sell`;
120
+ if (!v?.found)
121
+ return text(`Not enough comparable data to value that ${a.make} ${a.model} automatically. ` +
122
+ `Clara Carros can still make you a manual buy-out offer — want to get one? ` +
123
+ `Use contact_me, or go to ${sellUrl}`);
124
+ const conf = v.rough ? " (rough estimate — few comparables)" : "";
125
+ return text(`Estimated market value for ${a.make} ${a.model} ${a.year}, ${a.km.toLocaleString("en-US")} km${conf}:\n` +
126
+ `• Typical: ${eur(v.median)}\n• Range: ${eur(v.low)} – ${eur(v.high)}\n` +
127
+ (v.days_sell ? `• Avg. days to sell: ~${v.days_sell}\n` : "") +
128
+ `Based on ${v.comps} comparable listing(s). This is the market asking range.\n\n` +
129
+ `💶 Want to sell it? Clara Carros buys cars directly — fast, fair (above a trade-in, below a slow private sale). ` +
130
+ `Ask for a firm buy-out offer: call contact_me (with the user's name + phone/email), or visit ${sellUrl}`);
131
+ });
132
+ // ---- list_services ------------------------------------------------------
133
+ server.tool("list_services", "List what Clara Carros offers (buying stock cars, importing to order, sourcing, auction buying, selling your car, ISV calculator).", {}, async () => text(SERVICES.map((s) => `• ${s.name} — ${s.desc}\n ${s.url}`).join("\n")));
134
+ // ---- contact_me ---------------------------------------------------------
135
+ server.tool("contact_me", "Send a contact request to Clara Carros so a human gets in touch. Only call this when the user explicitly asks to be contacted, provides their own name and phone or email, AND agrees to share those details with Clara Carros — pass consent=true only then. Submitting shares the provided contact details per Clara Carros' privacy policy.", {
136
+ name: z.string().describe("The person's name (as they gave it)"),
137
+ phone: z.string().optional().describe("Phone number (phone OR email required)"),
138
+ email: z.string().optional().describe("Email (phone OR email required)"),
139
+ message: z.string().optional().describe("What they want / their question"),
140
+ consent: z.boolean().describe("Set true ONLY if the user explicitly agreed to share their contact details and be contacted"),
141
+ }, async (a) => {
142
+ if (!a.consent)
143
+ return text("Before sending, the user must explicitly agree to share their contact details with Clara Carros. Confirm consent, then call again with consent=true.");
144
+ if (!a.phone && !a.email)
145
+ return text("A phone number or email is required to send a contact request. Ask the user for one.");
146
+ const r = await apiPost("/lead", {
147
+ kind: "contact", name: a.name, phone: a.phone, email: a.email,
148
+ message: a.message, lang: DEFAULT_LANG, source: "mcp", consent: true,
149
+ });
150
+ if (r?.duplicate)
151
+ return text("You already sent this request — Clara Carros has it and will be in touch.");
152
+ return text("Sent ✓ Clara Carros received your request and will contact you soon.");
153
+ });
154
+ // ---- request_import_quote ----------------------------------------------
155
+ server.tool("request_import_quote", "Ask Clara Carros to source & quote a specific car to import (from German/EU auctions). Only call when the user wants an import quote, provides the wanted model plus their own name and phone or email, AND agrees to be contacted — pass consent=true only then. Submitting shares the provided contact details per Clara Carros' privacy policy.", {
156
+ model: z.string().describe("Wanted car, e.g. 'BMW 320d Touring 2021'"),
157
+ budget: z.number().optional().describe("Budget in EUR"),
158
+ name: z.string().describe("The person's name"),
159
+ phone: z.string().optional().describe("Phone (phone OR email required)"),
160
+ email: z.string().optional().describe("Email (phone OR email required)"),
161
+ consent: z.boolean().describe("Set true ONLY if the user explicitly agreed to share their contact details and be contacted"),
162
+ }, async (a) => {
163
+ if (!a.consent)
164
+ return text("Before sending, the user must explicitly agree to share their contact details with Clara Carros. Confirm consent, then call again with consent=true.");
165
+ if (!a.phone && !a.email)
166
+ return text("A phone number or email is required to request an import quote. Ask the user for one.");
167
+ const msg = `Import request: ${a.model}${a.budget ? ` (budget €${a.budget.toLocaleString("en-US")})` : ""}`;
168
+ const r = await apiPost("/lead", {
169
+ kind: "import", name: a.name, phone: a.phone, email: a.email,
170
+ message: msg, lang: DEFAULT_LANG, source: "mcp", consent: true,
171
+ });
172
+ if (r?.duplicate)
173
+ return text("You already sent this import request — Clara Carros has it and will be in touch.");
174
+ return text(`Sent ✓ Clara Carros will source "${a.model}" and get back to you with a transparent total price.`);
175
+ });
176
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "claracars-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Clara Carros — search used & imported cars in Portugal, calculate import tax (ISV), estimate resale value, and get in touch. Thin wrapper over the public claracars.pt API.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "claracars-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ ".well-known"
13
+ ],
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "cars",
18
+ "portugal",
19
+ "used-cars",
20
+ "car-import",
21
+ "isv",
22
+ "automotive"
23
+ ],
24
+ "homepage": "https://claracars.pt",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/claracarspt/mcp"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "dev": "tsc --watch",
32
+ "start": "node dist/index.js",
33
+ "start:http": "node dist/http.js",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.0.4",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "typescript": "^5.5.0"
46
+ }
47
+ }