236 lines
8.4 KiB
TypeScript

/**
* Pricing tools: get_pricing, compare_prices, get_competitor_stock
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { pool } from "../db.js";
export async function registerPricingTools(server: McpServer): Promise<void> {
// --- Tool: get_pricing ---
server.tool(
"get_pricing",
"Get current prices and availability across all sources for a specific transceiver. Includes price history if requested.",
{
part_number: z.string().describe("Part number or slug, e.g. 'qsfp28-lr4' or 'SFP-10G-LR'"),
vendor: z.string().optional().describe("Filter to specific vendor only"),
include_history: z.boolean().default(false).describe("Include 30-day price history"),
},
async ({ part_number, vendor, include_history }) => {
// Find transceiver
const txResult = await pool.query(
`SELECT t.id, t.slug, t.standard_name, t.form_factor, t.speed, t.reach_label
FROM transceivers t
WHERE t.slug ILIKE $1 OR t.part_number ILIKE $1 OR t.standard_name ILIKE $1
LIMIT 1`,
[`%${part_number}%`]
);
if (txResult.rows.length === 0) {
return {
content: [{ type: "text", text: `No transceiver found for "${part_number}".` }],
};
}
const tx = txResult.rows[0];
const vendorCondition = vendor ? `AND v.name ILIKE $2` : "";
const baseParams: unknown[] = vendor ? [tx.id, `%${vendor}%`] : [tx.id];
// Current prices (latest per vendor)
const currentPrices = await pool.query(
`SELECT DISTINCT ON (po.source_vendor_id)
v.name as vendor, po.price, po.currency, po.stock_level,
po.quantity_available, po.url, po.time
FROM price_observations po
JOIN vendors v ON v.id = po.source_vendor_id
WHERE po.transceiver_id = $1 ${vendorCondition}
ORDER BY po.source_vendor_id, po.time DESC`,
baseParams
);
let history = null;
if (include_history) {
const histResult = await pool.query(
`SELECT v.name as vendor, po.price, po.currency, po.stock_level, po.time
FROM price_observations po
JOIN vendors v ON v.id = po.source_vendor_id
WHERE po.transceiver_id = $1 ${vendorCondition}
AND po.time > NOW() - INTERVAL '30 days'
ORDER BY po.time DESC
LIMIT 200`,
baseParams
);
history = histResult.rows;
}
const response = {
transceiver: {
slug: tx.slug,
standard: tx.standard_name,
form_factor: tx.form_factor,
speed: tx.speed,
reach: tx.reach_label,
},
current_prices: currentPrices.rows,
cheapest: currentPrices.rows.length > 0
? currentPrices.rows.reduce((min, r) => r.price < min.price ? r : min)
: null,
history: include_history ? history : undefined,
};
return {
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
};
}
);
// --- Tool: compare_prices ---
server.tool(
"compare_prices",
"Compare prices across all vendors for a transceiver. Shows savings opportunities vs competitors.",
{
transceiver_query: z.string().describe("Free text or part number to search for"),
competitors: z.array(z.string()).optional().describe("Limit to specific competitors"),
},
async ({ transceiver_query, competitors }) => {
const vendorFilter = competitors && competitors.length > 0
? `AND v.name ILIKE ANY(ARRAY[${competitors.map((_, i) => `$${i + 2}`).join(",")}])`
: "";
const values: unknown[] = [transceiver_query];
if (competitors) {
competitors.forEach((c) => values.push(`%${c}%`));
}
const result = await pool.query(
`SELECT t.slug, t.standard_name, t.form_factor, t.speed, t.reach_label,
v.name as vendor, v.type,
po.price, po.currency, po.stock_level, po.time
FROM price_observations po
JOIN transceivers t ON t.id = po.transceiver_id
JOIN vendors v ON v.id = po.source_vendor_id
WHERE (t.standard_name ILIKE $1 OR t.slug ILIKE $1)
${vendorFilter}
AND po.time = (
SELECT MAX(time) FROM price_observations
WHERE transceiver_id = t.id AND source_vendor_id = v.id
)
ORDER BY t.slug, po.price ASC`,
values
);
if (result.rows.length === 0) {
return {
content: [{ type: "text", text: `No pricing data found for "${transceiver_query}".` }],
};
}
// Group by transceiver
const grouped: Record<string, {
transceiver: { slug: string; standard: string; form_factor: string; speed: string; reach: string };
prices: Array<{ vendor: string; type: string; price: number; currency: string; stock: string }>;
min_price: number;
max_price: number;
savings_vs_max: number;
}> = {};
for (const row of result.rows) {
if (!grouped[row.slug]) {
grouped[row.slug] = {
transceiver: {
slug: row.slug,
standard: row.standard_name,
form_factor: row.form_factor,
speed: row.speed,
reach: row.reach_label,
},
prices: [],
min_price: Infinity,
max_price: 0,
savings_vs_max: 0,
};
}
grouped[row.slug].prices.push({
vendor: row.vendor,
type: row.type,
price: parseFloat(row.price),
currency: row.currency,
stock: row.stock_level,
});
grouped[row.slug].min_price = Math.min(grouped[row.slug].min_price, parseFloat(row.price));
grouped[row.slug].max_price = Math.max(grouped[row.slug].max_price, parseFloat(row.price));
}
for (const slug of Object.keys(grouped)) {
const g = grouped[slug];
g.savings_vs_max = Math.round((1 - g.min_price / g.max_price) * 100);
}
return {
content: [{
type: "text",
text: JSON.stringify({ comparisons: Object.values(grouped) }, null, 2),
}],
};
}
);
// --- Tool: get_competitor_stock ---
server.tool(
"get_competitor_stock",
"Check live stock levels at a specific competitor. Useful for identifying sales opportunities when competitor is out of stock.",
{
competitor: z.string().describe("Competitor name, e.g. 'FS.COM', 'Optcore', 'ProLabs'"),
product_query: z.string().optional().describe("Optional product filter"),
out_of_stock_only: z.boolean().default(false).describe("Only show out-of-stock items (sales opportunities)"),
},
async ({ competitor, product_query, out_of_stock_only }) => {
const conditions = [`v.name ILIKE $1`];
const values: unknown[] = [`%${competitor}%`];
let idx = 2;
if (product_query) {
conditions.push(`(t.standard_name ILIKE $${idx} OR t.slug ILIKE $${idx})`);
values.push(`%${product_query}%`);
idx++;
}
if (out_of_stock_only) {
conditions.push(`po.stock_level IN ('out_of_stock', 'discontinued')`);
}
const result = await pool.query(
`SELECT DISTINCT ON (t.id)
v.name as competitor, t.slug, t.standard_name, t.form_factor, t.speed,
t.reach_label, po.price, po.currency, po.stock_level,
po.quantity_available, po.time
FROM price_observations po
JOIN vendors v ON v.id = po.source_vendor_id
JOIN transceivers t ON t.id = po.transceiver_id
WHERE ${conditions.join(" AND ")}
ORDER BY t.id, po.time DESC
LIMIT 100`,
values
);
const inStock = result.rows.filter((r) => r.stock_level === "in_stock").length;
const outOfStock = result.rows.filter((r) =>
["out_of_stock", "discontinued"].includes(r.stock_level)
).length;
return {
content: [{
type: "text",
text: JSON.stringify({
competitor,
summary: { total: result.rows.length, in_stock: inStock, out_of_stock: outOfStock },
items: result.rows,
sales_opportunities: out_of_stock_only
? result.rows.length
: `${outOfStock} items out of stock at ${competitor}`,
}, null, 2),
}],
};
}
);
}