ifandco-mcp 1.0.1

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,16 @@
1
+ # ifandco-mcp
2
+
3
+ Query the IF & Co. catalog (ifandco.com, NYC): search custom pendants, chains, rings and bracelets, newest arrivals, enriched product details, price stats, below-median deals, and category counts. Public storefront data, no API key required. Not affiliated with IF & Co.
4
+
5
+ ## Tools
6
+
7
+ - `search_products` — Keyword search across the catalog.
8
+ - `list_newest` — Newest catalog products, sorted by publish date.
9
+ - `get_product` — Full details: variants with parsed options, images, description, tags.
10
+ - `price_overview` — Ask-price distribution (min/p25/median/p75/max).
11
+ - `find_deals` — Available products priced below the median.
12
+ - `list_categories` — Product-type counts in the catalog sample.
13
+
14
+ ## Source
15
+
16
+ [ifandco-mcp](https://github.com/mrfentmen/premium-mcps/tree/main/servers/ifandco-mcp)
package/dist/api.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export declare class IfAndCoError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare function errorMessage(e: unknown): string;
5
+ export declare function searchProducts(query: string, limit?: string, maxPrice?: string, productType?: string): Promise<string>;
6
+ export declare function listNewest(limit?: string, page?: string, productType?: string): Promise<string>;
7
+ export declare function getProduct(handle: string): Promise<string>;
8
+ export declare function priceOverview(limit?: string): Promise<string>;
9
+ export declare function findDeals(limit?: string, sample?: string): Promise<string>;
10
+ export declare function listCategories(): Promise<string>;
package/dist/api.js ADDED
@@ -0,0 +1,176 @@
1
+ export class IfAndCoError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "IfAndCoError";
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 CATALOG = "https://ifandco.com/products.json";
12
+ const SUGGEST = "https://ifandco.com/search/suggest.json";
13
+ const PRODUCT_BASE = "https://ifandco.com/products";
14
+ function pretty(data) {
15
+ const t = typeof data === "string" ? data : JSON.stringify(data, null, 2);
16
+ return t.length > 12000 ? t.slice(0, 12000) + "\n…(truncated)" : t;
17
+ }
18
+ async function getJson(url) {
19
+ let res;
20
+ try {
21
+ res = await fetch(url, { headers: { ...UA } });
22
+ }
23
+ catch (e) {
24
+ throw new IfAndCoError(`Network error: ${e.message}`);
25
+ }
26
+ if (!res.ok)
27
+ throw new IfAndCoError(`HTTP ${res.status} for ${new URL(url).hostname}`);
28
+ return res.json();
29
+ }
30
+ function slim(p, cents = false) {
31
+ const variants = Array.isArray(p.variants) ? p.variants : [];
32
+ const varPrices = variants.map((v) => Number(v.price)).filter((n) => Number.isFinite(n));
33
+ const topPrice = Number(p.price ?? p.price_min);
34
+ const raw = varPrices.length ? varPrices : (Number.isFinite(topPrice) ? [topPrice] : []);
35
+ const prices = cents ? raw.map((x) => Math.round(x) / 100) : raw;
36
+ const images = Array.isArray(p.images) ? p.images : [];
37
+ const img0 = images.length ? images[0] : null;
38
+ return {
39
+ id: p.id,
40
+ title: p.title,
41
+ handle: p.handle,
42
+ vendor: p.vendor,
43
+ product_type: p.product_type,
44
+ price_usd: prices.length ? Math.min(...prices) : null,
45
+ available: variants.some((v) => v.available) || p.available === true,
46
+ url: `${PRODUCT_BASE}/${p.handle}`,
47
+ image: typeof img0 === "string" ? img0 : (img0?.src ?? null),
48
+ published_at: p.published_at ?? null,
49
+ };
50
+ }
51
+ function passes(s, maxPrice, productType) {
52
+ if (maxPrice !== undefined && (s.price_usd === null || s.price_usd > maxPrice))
53
+ return false;
54
+ if (productType && !s.product_type.toLowerCase().includes(productType.toLowerCase()))
55
+ return false;
56
+ return true;
57
+ }
58
+ async function catalogPage(page) {
59
+ const data = await getJson(`${CATALOG}?limit=250&page=${page}`);
60
+ return Array.isArray(data.products) ? data.products : [];
61
+ }
62
+ async function sampleCatalog(maxItems, pages) {
63
+ const out = [];
64
+ for (const pg of pages) {
65
+ if (out.length >= maxItems)
66
+ break;
67
+ try {
68
+ for (const p of await catalogPage(pg)) {
69
+ out.push(p);
70
+ if (out.length >= maxItems)
71
+ break;
72
+ }
73
+ }
74
+ catch { /* page failed — keep what we have */ }
75
+ }
76
+ return out;
77
+ }
78
+ export async function searchProducts(query, limit, maxPrice, productType) {
79
+ const n = Math.min(Number(limit ?? 10) || 10, 25);
80
+ const mp = maxPrice !== undefined ? Number(maxPrice) : undefined;
81
+ const data = await getJson(`${SUGGEST}?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=${n}`);
82
+ const products = data?.resources?.results?.products ?? [];
83
+ const out = [];
84
+ for (const p of products) {
85
+ const sl = slim(p);
86
+ if (passes(sl, mp, productType))
87
+ out.push(sl);
88
+ if (out.length >= n)
89
+ break;
90
+ }
91
+ return pretty({ store: "IF & Co.", query, count: out.length, listings: out });
92
+ }
93
+ export async function listNewest(limit, page, productType) {
94
+ const n = Math.min(Number(limit ?? 20) || 20, 50);
95
+ const pg = Math.max(Number(page ?? 1) || 1, 1);
96
+ const out = [];
97
+ for (const p of await catalogPage(pg)) {
98
+ const sl = slim(p);
99
+ if (sl.product_type && (!productType || sl.product_type.toLowerCase().includes(productType.toLowerCase())))
100
+ out.push(sl);
101
+ }
102
+ out.sort((a, b) => String(b.published_at ?? "").localeCompare(String(a.published_at ?? "")));
103
+ return pretty({ store: "IF & Co.", page: pg, count: out.length, listings: out.slice(0, n) });
104
+ }
105
+ function parseParts(title) {
106
+ return String(title ?? "").split("/").map((s) => s.trim()).filter(Boolean);
107
+ }
108
+ export async function getProduct(handle) {
109
+ const p = await getJson(`${PRODUCT_BASE}/${encodeURIComponent(handle)}.js`);
110
+ const sl = slim(p, true);
111
+ const variants = (Array.isArray(p.variants) ? p.variants : []).map((v) => ({
112
+ title: v.title, parts: parseParts(v.title),
113
+ price_usd: Math.round(Number(v.price)) / 100,
114
+ compare_at_usd: v.compare_at_price !== null && v.compare_at_price !== undefined ? Math.round(Number(v.compare_at_price)) / 100 : null,
115
+ available: v.available,
116
+ }));
117
+ const images = (Array.isArray(p.images) ? p.images : []).map((i) => typeof i === "string" ? i : i.src).filter(Boolean);
118
+ const html = typeof p.body_html === "string" ? p.body_html : (typeof p.description === "string" ? p.description : "");
119
+ const dollar = variants.length && Number.isFinite(variants[0].price_usd) ? variants[0].price_usd : (sl.price_usd !== null ? Math.round(sl.price_usd) / 100 : null);
120
+ return pretty({
121
+ store: "IF & Co.", ...sl, price_usd: dollar, variants, images,
122
+ description: html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 1500) || null,
123
+ tags: p.tags,
124
+ });
125
+ }
126
+ export async function priceOverview(limit) {
127
+ const n = Math.min(Number(limit ?? 200) || 200, 500);
128
+ const prices = [];
129
+ for (const p of await sampleCatalog(n, [1, 2, 3])) {
130
+ const sl = slim(p);
131
+ if (sl.price_usd !== null)
132
+ prices.push(sl.price_usd);
133
+ }
134
+ if (!prices.length)
135
+ throw new IfAndCoError("No priced products found.");
136
+ const a = [...prices].sort((x, y) => x - y);
137
+ const mid = a.length % 2 ? a[(a.length - 1) / 2] : (a[a.length / 2 - 1] + a[a.length / 2]) / 2;
138
+ return pretty({
139
+ store: "IF & Co.", currency: "USD", sample: a.length,
140
+ min: a[0], p25: a[Math.floor(a.length * 0.25)], median: mid,
141
+ p75: a[Math.floor(a.length * 0.75)], max: a[a.length - 1],
142
+ note: "Storefront asking prices. Not affiliated with IF & Co..",
143
+ });
144
+ }
145
+ export async function findDeals(limit, sample) {
146
+ const n = Math.min(Number(limit ?? 10) || 10, 25);
147
+ const per = Math.min(Number(sample ?? 200) || 200, 500);
148
+ const all = [];
149
+ for (const p of await sampleCatalog(per, [1, 2, 3])) {
150
+ const sl = slim(p);
151
+ if (sl.price_usd !== null && sl.available)
152
+ all.push(sl);
153
+ }
154
+ if (!all.length)
155
+ throw new IfAndCoError("No available priced products found.");
156
+ const ps = all.map((s) => s.price_usd).sort((a, b) => a - b);
157
+ const median = ps.length % 2 ? ps[(ps.length - 1) / 2] : (ps[ps.length / 2 - 1] + ps[ps.length / 2]) / 2;
158
+ const JUNK = /protection plan|warranty|gift card|shipping|handling fee|service plan/i;
159
+ const deals = all
160
+ .filter((s) => s.price_usd < median && !JUNK.test(s.title))
161
+ .map((s) => ({ ...s, vs_median_usd: Math.round((s.price_usd - median) * 100) / 100, vs_median_pct: Math.round(((s.price_usd - median) / median) * 1000) / 10 }))
162
+ .sort((a, b) => a.vs_median_pct - b.vs_median_pct);
163
+ return pretty({
164
+ store: "IF & Co.", sample: all.length, median_usd: median, count: Math.min(deals.length, n),
165
+ deals: deals.slice(0, n),
166
+ note: "Below-median asks, not appraisals. Not affiliated with IF & Co..",
167
+ });
168
+ }
169
+ export async function listCategories() {
170
+ const counts = {};
171
+ for (const p of await sampleCatalog(500, [1, 2])) {
172
+ const t = p.product_type || "unknown";
173
+ counts[t] = (counts[t] ?? 0) + 1;
174
+ }
175
+ return pretty({ store: "IF & Co.", sample: Object.values(counts).reduce((a, b) => a + b, 0), categories: counts });
176
+ }
@@ -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,107 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { errorMessage, searchProducts, listNewest, getProduct, priceOverview, findDeals, listCategories, } 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: "ifandco-mcp",
10
+ version: "1.0.0",
11
+ });
12
+ server.registerTool("search_products", {
13
+ title: "Search products",
14
+ description: "Keyword search across the catalog.",
15
+ inputSchema: z.object({
16
+ query: z.string().describe("Keyword, e.g. pendant, cuban link, rolex."),
17
+ limit: z.string().describe("Max results (default 10, max 25).").optional(),
18
+ max_price: z.string().describe("Max USD price.").optional(),
19
+ product_type: z.string().describe("Filter by type.").optional()
20
+ }),
21
+ annotations: READ_ONLY,
22
+ }, async ({ query, limit, max_price: maxPrice, product_type: productType }) => {
23
+ try {
24
+ return text(await searchProducts(query, limit, maxPrice, productType));
25
+ }
26
+ catch (e) {
27
+ return textError(errorMessage(e));
28
+ }
29
+ });
30
+ server.registerTool("list_newest", {
31
+ title: "Newest arrivals",
32
+ description: "Newest catalog products, sorted by publish date.",
33
+ inputSchema: z.object({
34
+ limit: z.string().describe("Max results (default 20, max 50).").optional(),
35
+ page: z.string().describe("Catalog page (default 1).").optional(),
36
+ product_type: z.string().describe("Filter by type.").optional()
37
+ }),
38
+ annotations: READ_ONLY,
39
+ }, async ({ limit, page, product_type: productType }) => {
40
+ try {
41
+ return text(await listNewest(limit, page, productType));
42
+ }
43
+ catch (e) {
44
+ return textError(errorMessage(e));
45
+ }
46
+ });
47
+ server.registerTool("get_product", {
48
+ title: "Get product",
49
+ description: "Full details: variants with parsed options, images, description, tags.",
50
+ inputSchema: z.object({
51
+ handle: z.string().describe("Product handle/slug.")
52
+ }),
53
+ annotations: READ_ONLY,
54
+ }, async ({ handle }) => {
55
+ try {
56
+ return text(await getProduct(handle));
57
+ }
58
+ catch (e) {
59
+ return textError(errorMessage(e));
60
+ }
61
+ });
62
+ server.registerTool("price_overview", {
63
+ title: "Price overview",
64
+ description: "Ask-price distribution (min/p25/median/p75/max).",
65
+ inputSchema: z.object({
66
+ limit: z.string().describe("Sample size (default 200, max 500).").optional()
67
+ }),
68
+ annotations: READ_ONLY,
69
+ }, async ({ limit }) => {
70
+ try {
71
+ return text(await priceOverview(limit));
72
+ }
73
+ catch (e) {
74
+ return textError(errorMessage(e));
75
+ }
76
+ });
77
+ server.registerTool("find_deals", {
78
+ title: "Find deals",
79
+ description: "Available products priced below the median.",
80
+ inputSchema: z.object({
81
+ limit: z.string().describe("Max deals (default 10, max 25).").optional(),
82
+ sample: z.string().describe("Products sampled (default 200, max 500).").optional()
83
+ }),
84
+ annotations: READ_ONLY,
85
+ }, async ({ limit, sample }) => {
86
+ try {
87
+ return text(await findDeals(limit, sample));
88
+ }
89
+ catch (e) {
90
+ return textError(errorMessage(e));
91
+ }
92
+ });
93
+ server.registerTool("list_categories", {
94
+ title: "Categories",
95
+ description: "Product-type counts in the catalog sample.",
96
+ inputSchema: z.object({}),
97
+ annotations: READ_ONLY,
98
+ }, async ({}) => {
99
+ try {
100
+ return text(await listCategories());
101
+ }
102
+ catch (e) {
103
+ return textError(errorMessage(e));
104
+ }
105
+ });
106
+ return server;
107
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "ifandco-mcp",
3
+ "version": "1.0.1",
4
+ "description": "IF & Co. jewelry: search, products, prices. Keyless.",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/ifandco-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mrfentmen/premium-mcps.git"
10
+ },
11
+ "bin": {
12
+ "ifandco-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
+ "if-and-co",
27
+ "jewelry",
28
+ "diamonds",
29
+ "pendants"
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
+ }