fendi-mcp 1.0.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.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # fendi-mcp
2
+
3
+ Fendi resale listings aggregated from Fashionphile and Rebag public catalogs (prices in USD). No API key required. Not affiliated with or endorsed by Fendi; prices are asking prices, not appraisals.
4
+
5
+ ## Tools
6
+
7
+ - `search_listings` — Search resale listings by keyword across Fashionphile and Rebag.
8
+ - `list_newest` — Newest catalog listings from both resale sources.
9
+ - `get_listing` — Full listing details by handle.
10
+ - `price_overview` — Ask-price distribution (min/p25/median/p75/max). Asks, not appraisals.
11
+
12
+ ## Source
13
+
14
+ [fendi-mcp](https://github.com/mrfentmen/awesome-mcps/tree/main/servers/fendi-mcp)
package/dist/api.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export declare class FendiError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare function errorMessage(e: unknown): string;
5
+ export declare function searchListings(query: string, limit?: string): Promise<string>;
6
+ export declare function listNewest(limit?: string, page?: string): Promise<string>;
7
+ export declare function getListing(handle: string, source?: string): Promise<string>;
8
+ export declare function priceOverview(limit?: string): Promise<string>;
package/dist/api.js ADDED
@@ -0,0 +1,145 @@
1
+ export class FendiError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "FendiError";
5
+ }
6
+ }
7
+ export function errorMessage(e) {
8
+ return e instanceof Error ? e.message : String(e);
9
+ }
10
+ const UA = { "User-Agent": "awesome-mcps/1.0" };
11
+ const VENDORS = ["fendi"];
12
+ const SOURCES = [
13
+ {
14
+ name: "fashionphile",
15
+ catalog: "https://www.fashionphile.com/products.json",
16
+ suggest: "https://www.fashionphile.com/search/suggest.json",
17
+ product: (handle) => `https://www.fashionphile.com/products/${encodeURIComponent(handle)}.js`,
18
+ },
19
+ {
20
+ name: "rebag",
21
+ catalog: "https://shop.rebag.com/products.json",
22
+ suggest: "https://shop.rebag.com/search/suggest.json",
23
+ product: (handle) => `https://shop.rebag.com/products/${encodeURIComponent(handle)}.js`,
24
+ },
25
+ ];
26
+ function matchesBrand(vendor) {
27
+ const v = String(vendor ?? "").toLowerCase();
28
+ return VENDORS.some((s) => v.includes(s));
29
+ }
30
+ function slim(source, base, p) {
31
+ const variants = Array.isArray(p.variants) ? p.variants : [];
32
+ const prices = variants.map((v) => Number(v.price)).filter((n) => Number.isFinite(n));
33
+ const comps = variants.map((v) => Number(v.compare_at_price)).filter((n) => Number.isFinite(n));
34
+ const images = Array.isArray(p.images) ? p.images : [];
35
+ return {
36
+ source,
37
+ id: p.id,
38
+ title: p.title,
39
+ handle: p.handle,
40
+ vendor: p.vendor,
41
+ product_type: p.product_type,
42
+ price_usd: prices.length ? Math.min(...prices) : null,
43
+ compare_at_usd: comps.length ? Math.min(...comps) : null,
44
+ available: variants.some((v) => v.available),
45
+ url: `${base}/${p.handle}`,
46
+ image: images.length ? images[0].src ?? null : null,
47
+ };
48
+ }
49
+ function pretty(data) {
50
+ const t = typeof data === "string" ? data : JSON.stringify(data, null, 2);
51
+ return t.length > 12000 ? t.slice(0, 12000) + "\n…(truncated)" : t;
52
+ }
53
+ async function getJson(url) {
54
+ let res;
55
+ try {
56
+ res = await fetch(url, { headers: { ...UA } });
57
+ }
58
+ catch (e) {
59
+ throw new FendiError(`Network error: ${e.message}`);
60
+ }
61
+ if (!res.ok)
62
+ throw new FendiError(`HTTP ${res.status} for ${new URL(url).hostname}`);
63
+ return res.json();
64
+ }
65
+ async function catalog(source, page) {
66
+ const data = await getJson(`${source.catalog}?limit=250&page=${page}`);
67
+ const products = Array.isArray(data.products) ? data.products : [];
68
+ return products.filter((p) => matchesBrand(p.vendor));
69
+ }
70
+ export async function searchListings(query, limit) {
71
+ const n = Math.min(Number(limit ?? 10) || 10, 25);
72
+ const out = [];
73
+ for (const s of SOURCES) {
74
+ try {
75
+ const data = await getJson(`${s.suggest}?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=${n}`);
76
+ const products = data?.resources?.results?.products ?? [];
77
+ const base = s.catalog.replace("/products.json", "");
78
+ for (const p of products) {
79
+ if (matchesBrand(p.vendor))
80
+ out.push(slim(s.name, base, p));
81
+ if (out.length >= n)
82
+ break;
83
+ }
84
+ }
85
+ catch { /* source down — keep the other */ }
86
+ if (out.length >= n)
87
+ break;
88
+ }
89
+ return pretty({ brand: "Fendi", query, count: out.length, listings: out.slice(0, n) });
90
+ }
91
+ export async function listNewest(limit, page) {
92
+ const n = Math.min(Number(limit ?? 20) || 20, 50);
93
+ const pg = Math.max(Number(page ?? 1) || 1, 1);
94
+ const out = [];
95
+ for (const s of SOURCES) {
96
+ try {
97
+ const base = s.catalog.replace("/products.json", "");
98
+ for (const p of await catalog(s, pg))
99
+ out.push(slim(s.name, base, p));
100
+ }
101
+ catch { /* source down — keep the other */ }
102
+ }
103
+ return pretty({ brand: "Fendi", page: pg, count: out.length, listings: out.slice(0, n) });
104
+ }
105
+ export async function getListing(handle, source) {
106
+ const sources = source ? SOURCES.filter((s) => s.name === source) : SOURCES;
107
+ if (!sources.length)
108
+ throw new FendiError(`Unknown source '${source}'. Use fashionphile or rebag.`);
109
+ for (const s of sources) {
110
+ try {
111
+ return pretty({ brand: "Fendi", source: s.name, product: await getJson(s.product(handle)) });
112
+ }
113
+ catch { /* try next source */ }
114
+ }
115
+ throw new FendiError(`Listing '${handle}' not found on ${sources.map((s) => s.name).join(", ")}.`);
116
+ }
117
+ export async function priceOverview(limit) {
118
+ const n = Math.min(Number(limit ?? 200) || 200, 500);
119
+ const prices = [];
120
+ for (const s of SOURCES) {
121
+ try {
122
+ const base = s.catalog.replace("/products.json", "");
123
+ for (const p of await catalog(s, 1)) {
124
+ const sl = slim(s.name, base, p);
125
+ if (sl.price_usd !== null)
126
+ prices.push(sl.price_usd);
127
+ if (prices.length >= n)
128
+ break;
129
+ }
130
+ }
131
+ catch { /* source down — keep the other */ }
132
+ if (prices.length >= n)
133
+ break;
134
+ }
135
+ if (!prices.length)
136
+ throw new FendiError("No priced listings found.");
137
+ prices.sort((a, b) => a - b);
138
+ const mid = (arr) => arr.length % 2 ? arr[(arr.length - 1) / 2] : (arr[arr.length / 2 - 1] + arr[arr.length / 2]) / 2;
139
+ return pretty({
140
+ brand: "Fendi", currency: "USD", sample: prices.length,
141
+ min: prices[0], p25: prices[Math.floor(prices.length * 0.25)],
142
+ median: mid(prices), p75: prices[Math.floor(prices.length * 0.75)], max: prices[prices.length - 1],
143
+ note: "Asking prices on resale sources, not appraisals. Not affiliated with Fendi.",
144
+ });
145
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ async function main() {
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ console.error("MCP server running on stdio");
8
+ }
9
+ main().catch((err) => {
10
+ console.error("Fatal error:", err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,76 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { errorMessage, searchListings, listNewest, getListing, priceOverview, } from "./api.js";
4
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ export function createServer() {
8
+ const server = new McpServer({
9
+ name: "fendi-mcp",
10
+ version: "1.0.0",
11
+ });
12
+ server.registerTool("search_listings", {
13
+ title: "Search listings",
14
+ description: "Search resale listings by keyword across Fashionphile and Rebag.",
15
+ inputSchema: z.object({
16
+ query: z.string().describe("Keyword, e.g. speedy, birkin, submariner."),
17
+ limit: z.string().describe("Max results (default 10, max 25).").optional()
18
+ }),
19
+ annotations: READ_ONLY,
20
+ }, async ({ query, limit }) => {
21
+ try {
22
+ return text(await searchListings(query, limit));
23
+ }
24
+ catch (e) {
25
+ return textError(errorMessage(e));
26
+ }
27
+ });
28
+ server.registerTool("list_newest", {
29
+ title: "Newest listings",
30
+ description: "Newest catalog listings from both resale sources.",
31
+ inputSchema: z.object({
32
+ limit: z.string().describe("Max results (default 20, max 50).").optional(),
33
+ page: z.string().describe("Catalog page (default 1).").optional()
34
+ }),
35
+ annotations: READ_ONLY,
36
+ }, async ({ limit, page }) => {
37
+ try {
38
+ return text(await listNewest(limit, page));
39
+ }
40
+ catch (e) {
41
+ return textError(errorMessage(e));
42
+ }
43
+ });
44
+ server.registerTool("get_listing", {
45
+ title: "Get listing",
46
+ description: "Full listing details by handle.",
47
+ inputSchema: z.object({
48
+ handle: z.string().describe("Product handle/slug."),
49
+ source: z.string().describe("fashionphile or rebag (tries both).").optional()
50
+ }),
51
+ annotations: READ_ONLY,
52
+ }, async ({ handle, source }) => {
53
+ try {
54
+ return text(await getListing(handle, source));
55
+ }
56
+ catch (e) {
57
+ return textError(errorMessage(e));
58
+ }
59
+ });
60
+ server.registerTool("price_overview", {
61
+ title: "Price overview",
62
+ description: "Ask-price distribution (min/p25/median/p75/max). Asks, not appraisals.",
63
+ inputSchema: z.object({
64
+ limit: z.string().describe("Sample size (default 200, max 500).").optional()
65
+ }),
66
+ annotations: READ_ONLY,
67
+ }, async ({ limit }) => {
68
+ try {
69
+ return text(await priceOverview(limit));
70
+ }
71
+ catch (e) {
72
+ return textError(errorMessage(e));
73
+ }
74
+ });
75
+ return server;
76
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "fendi-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Fendi resale listings from Fashionphile and Rebag. Keyless.",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/fendi-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mrfentmen/awesome-mcps.git"
10
+ },
11
+ "bin": {
12
+ "fendi-mcp": "./dist/index.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "start": "node dist/index.js",
21
+ "dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
22
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "fendi",
27
+ "luxury",
28
+ "resale",
29
+ "fashion"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.4",
34
+ "zod": "^3.23.8"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "typescript": "^5.6.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ }
43
+ }