279 lines
11 KiB
TypeScript

/**
* Knowledge tools: search_knowledge_base, search_manuals, get_hype_cycle
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { pool } from "../db.js";
// Norton-Bass diffusion model (inline, since MCP server has separate build)
interface TechGen { name: string; speedGbps: number; formFactor: string; introYear: number; peakYear: number; p: number; q: number; m: number; k: number; t0: number; }
const TECH_GENS: TechGen[] = [
{ name: "1G SFP", speedGbps: 1, formFactor: "SFP", introYear: 2001, peakYear: 2012, p: 0.03, q: 0.38, m: 500, k: 0.45, t0: 2008 },
{ name: "10G SFP+", speedGbps: 10, formFactor: "SFP+", introYear: 2006, peakYear: 2018, p: 0.03, q: 0.42, m: 600, k: 0.50, t0: 2014 },
{ name: "25G SFP28", speedGbps: 25, formFactor: "SFP28", introYear: 2015, peakYear: 2022, p: 0.04, q: 0.45, m: 200, k: 0.55, t0: 2019 },
{ name: "40G QSFP+", speedGbps: 40, formFactor: "QSFP+", introYear: 2010, peakYear: 2019, p: 0.025, q: 0.35, m: 150, k: 0.40, t0: 2016 },
{ name: "100G QSFP28", speedGbps: 100, formFactor: "QSFP28", introYear: 2014, peakYear: 2024, p: 0.03, q: 0.40, m: 400, k: 0.48, t0: 2020 },
{ name: "400G QSFP-DD", speedGbps: 400, formFactor: "QSFP-DD", introYear: 2020, peakYear: 2027, p: 0.035, q: 0.50, m: 300, k: 0.52, t0: 2025 },
{ name: "800G OSFP", speedGbps: 800, formFactor: "OSFP", introYear: 2023, peakYear: 2029, p: 0.04, q: 0.55, m: 250, k: 0.55, t0: 2027 },
{ name: "1.6T OSFP-XD", speedGbps: 1600, formFactor: "OSFP-XD", introYear: 2025, peakYear: 2032, p: 0.03, q: 0.45, m: 180, k: 0.48, t0: 2030 },
{ name: "CPO", speedGbps: 1600, formFactor: "CPO", introYear: 2024, peakYear: 2033, p: 0.02, q: 0.30, m: 50, k: 0.35, t0: 2031 },
{ name: "400ZR Coherent", speedGbps: 400, formFactor: "QSFP-DD", introYear: 2021, peakYear: 2026, p: 0.04, q: 0.50, m: 80, k: 0.55, t0: 2024 },
{ name: "LPO", speedGbps: 800, formFactor: "LPO", introYear: 2024, peakYear: 2029, p: 0.035, q: 0.48, m: 100, k: 0.50, t0: 2027 },
];
function bassCumulative(t: number, p: number, q: number): number {
const pq = p + q;
const exp = Math.exp(-pq * t);
return (1 - exp) / (1 + (q / p) * exp);
}
function bassRate(t: number, p: number, q: number): number {
const pq = p + q;
const exp = Math.exp(-pq * t);
const d = 1 + (q / p) * exp;
return (pq * pq / q) * (exp / (d * d));
}
function classifyHypePhase(progressRatio: number): string {
if (progressRatio < 0.15) return "Innovation Trigger";
if (progressRatio < 0.35) return "Peak of Inflated Expectations";
if (progressRatio < 0.55) return "Trough of Disillusionment";
if (progressRatio < 0.85) return "Slope of Enlightenment";
if (progressRatio < 1.3) return "Plateau of Productivity";
return "Legacy / Decline";
}
function findTech(query: string): TechGen | undefined {
const q = query.toLowerCase().trim();
return TECH_GENS.find(t => t.name.toLowerCase().includes(q))
|| TECH_GENS.find(t => q.includes(t.formFactor.toLowerCase()))
|| (() => {
const m = q.match(/^(\d+(?:\.\d+)?)\s*(g|t)\b/i);
if (m) {
const gbps = m[2].toLowerCase() === "t" ? parseFloat(m[1]) * 1000 : parseFloat(m[1]);
return TECH_GENS.find(t => t.speedGbps === gbps);
}
return undefined;
})();
}
export async function registerKnowledgeTools(server: McpServer): Promise<void> {
// --- Tool: search_knowledge_base ---
server.tool(
"search_knowledge_base",
"Search troubleshooting guides, FAQs, and best practices. Use for diagnosing transceiver/switch issues.",
{
query: z.string().describe("Problem description or question, e.g. 'low Rx power QSFP28' or 'link flapping SFP+'"),
category: z.enum(["troubleshooting", "faq", "best_practice", "configuration"]).optional(),
severity: z.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by issue severity"),
},
async ({ query, category, severity }) => {
const conditions = [`kb.search_vector @@ plainto_tsquery('english', $1)`];
const values: unknown[] = [query];
let idx = 2;
if (category) {
conditions.push(`kb.category = $${idx}`);
values.push(category);
idx++;
}
if (severity) {
conditions.push(`kb.severity = $${idx}`);
values.push(severity);
idx++;
}
const result = await pool.query(
`SELECT kb.id, kb.title, kb.content, kb.category, kb.severity,
kb.affected_products, kb.resolution, kb.source_url,
ts_rank(kb.search_vector, plainto_tsquery('english', $1)) as rank
FROM knowledge_base kb
WHERE ${conditions.join(" AND ")}
ORDER BY rank DESC
LIMIT 10`,
values
);
if (result.rows.length === 0) {
return {
content: [{
type: "text",
text: `No knowledge base entries found for: "${query}". Knowledge base grows as vendor FAQs are scraped.`,
}],
};
}
return {
content: [{
type: "text",
text: JSON.stringify({ results: result.rows, count: result.rows.length }, null, 2),
}],
};
}
);
// --- Tool: search_manuals ---
server.tool(
"search_manuals",
"Search switch manuals, configuration guides, and compatibility lists. Returns relevant document excerpts.",
{
query: z.string().describe("Query, e.g. 'configure DWDM on Juniper MX' or 'QSFP28 installation guide'"),
vendor: z.string().optional().describe("Document vendor filter, e.g. 'Cisco', 'Juniper', 'Arista'"),
doc_type: z.enum(["manual", "config_guide", "compatibility_list", "datasheet", "release_notes"]).optional(),
},
async ({ query, vendor, doc_type }) => {
const conditions = [`d.search_vector @@ plainto_tsquery('english', $1)`];
const values: unknown[] = [query];
let idx = 2;
if (vendor) {
conditions.push(`v.name ILIKE $${idx}`);
values.push(`%${vendor}%`);
idx++;
}
if (doc_type) {
conditions.push(`d.doc_type = $${idx}`);
values.push(doc_type);
idx++;
}
const result = await pool.query(
`SELECT d.id, d.title, d.doc_type, d.version,
v.name as vendor, d.source_url, d.pages,
d.summary,
ts_rank(d.search_vector, plainto_tsquery('english', $1)) as rank
FROM documents d
LEFT JOIN vendors v ON v.id = d.vendor_id
WHERE ${conditions.join(" AND ")}
ORDER BY rank DESC
LIMIT 10`,
values
);
if (result.rows.length === 0) {
return {
content: [{
type: "text",
text: `No manuals found for: "${query}". Document library grows as manuals are processed by OCR pipeline.`,
}],
};
}
return {
content: [{
type: "text",
text: JSON.stringify({ results: result.rows, count: result.rows.length }, null, 2),
}],
};
}
);
// --- Tool: get_hype_cycle ---
server.tool(
"get_hype_cycle",
"Get Hype Cycle position and Norton-Bass diffusion forecast for a transceiver technology. Shows market maturity, adoption phase, and price trajectory.",
{
technology: z.string().describe("Technology to analyze, e.g. '800G OSFP', 'CPO', '400ZR', '100G LR4', 'DWDM'"),
include_forecast: z.boolean().default(true).describe("Include Norton-Bass 5-year adoption forecast"),
},
async ({ technology, include_forecast }) => {
// Look for market metrics data
const result = await pool.query(
`SELECT mm.time, mm.metric_type, mm.value,
mm.notes
FROM market_metrics mm
WHERE mm.technology ILIKE $1
OR mm.notes ILIKE $1
ORDER BY mm.time DESC
LIMIT 50`,
[`%${technology}%`]
);
// Also check news for recent developments
const newsResult = await pool.query(
`SELECT title, published_at, source, summary
FROM news_articles
WHERE search_vector @@ plainto_tsquery('english', $1)
OR title ILIKE $2
ORDER BY published_at DESC
LIMIT 5`,
[technology, `%${technology}%`]
);
// Norton-Bass Diffusion Model computation
const currentYear = new Date().getFullYear();
const tech = findTech(technology);
let hypeData: Record<string, unknown> | null = null;
let forecast: Record<string, unknown> | undefined = undefined;
if (tech) {
const t = Math.max(0, currentYear - tech.introYear);
const yearsToPeak = tech.peakYear - tech.introYear;
const progressRatio = t / yearsToPeak;
const adoption = bassCumulative(t, tech.p, tech.q);
const rate = bassRate(t, tech.p, tech.q);
const phase = classifyHypePhase(progressRatio);
// Position on curve (0-100) based on progress ratio
const positionPct = Math.min(100, Math.round(
progressRatio < 0.15 ? progressRatio / 0.15 * 15 :
progressRatio < 0.35 ? 15 + (progressRatio - 0.15) / 0.20 * 15 :
progressRatio < 0.55 ? 30 + (progressRatio - 0.35) / 0.20 * 20 :
progressRatio < 0.85 ? 50 + (progressRatio - 0.55) / 0.30 * 30 :
80 + Math.min(20, (progressRatio - 0.85) / 0.45 * 20)
));
hypeData = {
technology: tech.name,
phase,
position: positionPct,
adoption_pct: Math.round(adoption * 100),
yearly_adoption_rate: Math.round(rate * 100),
intro_year: tech.introYear,
peak_year: tech.peakYear,
years_to_plateau: Math.max(0, tech.peakYear + 3 - currentYear),
model: "Norton-Bass Multigenerational Diffusion",
parameters: { p: tech.p, q: tech.q, m: tech.m, k: tech.k, t0: tech.t0 },
};
if (include_forecast) {
const fiveYear = Array.from({ length: 5 }, (_, i) => {
const yr = currentYear + i + 1;
const ft = yr - tech.introYear;
const fa = bassCumulative(ft, tech.p, tech.q);
const pr = ft / yearsToPeak;
return { year: yr, adoption_pct: Math.round(fa * 100), phase: classifyHypePhase(pr) };
});
const aspDecline = progressRatio < 0.3 ? 5 : progressRatio < 0.6 ? 35 : progressRatio < 1.0 ? 15 : 5;
forecast = {
model: "Norton-Bass Multigenerational Diffusion",
current_adoption_pct: Math.round(adoption * 100),
estimated_peak_year: tech.peakYear,
years_to_plateau: Math.max(0, tech.peakYear + 3 - currentYear),
asp_decline_rate_pct: aspDecline,
price_trend: aspDecline > 20 ? "Rapidly declining" : aspDecline > 10 ? "Moderately declining" : "Stabilizing",
five_year_projection: fiveYear,
};
}
}
return {
content: [{
type: "text",
text: JSON.stringify({
technology,
hype_cycle: hypeData || { note: `No model data for "${technology}". Known: 1G, 10G, 25G, 40G, 100G, 400G, 800G, 1.6T, CPO, LPO, 400ZR` },
market_metrics: result.rows,
recent_news: newsResult.rows,
norton_bass_forecast: forecast,
}, null, 2),
}],
};
}
);
}