Rene Fichtmueller 9d9d9ed8ae feat: hype cycle hover tooltips + phase legend, fix switch-docs missing column
Dashboard: Added hover tooltips on hype cycle dots showing phase, adoption %,
peak year, score. Added color-coded phase legend with technology counts.
MCP: Fixed docs_portal_url column reference in switch-docs tool.
2026-03-30 08:25:41 +02:00

167 lines
6.5 KiB
TypeScript

/**
* Switch documentation tools: get_switch_docs, get_switch_image
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { pool } from "../db.js";
export async function registerSwitchDocTools(server: McpServer): Promise<void> {
// --- Tool: get_switch_docs ---
server.tool(
"get_switch_docs",
"Get datasheets, manuals, and documentation for a switch/router model. Returns links to PDFs, configuration guides, quick start guides, and CLI references.",
{
model: z.string().describe("Switch/router model name, e.g. 'N9K-C93600CD-GX' or 'CRS504'"),
doc_type: z.enum(["all", "datasheet", "manual", "quick_start", "cli_reference", "installation_guide"]).default("all").describe("Filter by document type"),
},
async ({ model, doc_type }) => {
// Find the switch
const switchResult = await pool.query(
`SELECT sw.id, sw.model, sw.series, sw.image_url, sw.datasheet_url,
sw.product_page_url, sw.manual_urls,
v.name as vendor_name, v.website as vendor_website
FROM switches sw
LEFT JOIN vendors v ON sw.vendor_id = v.id
WHERE sw.model ILIKE $1
LIMIT 5`,
[`%${model}%`]
);
if (switchResult.rows.length === 0) {
return {
content: [{ type: "text", text: `No switch found matching "${model}". Try a more specific model name.` }],
};
}
const results: string[] = [];
for (const sw of switchResult.rows) {
// Get associated documents
const docQuery = doc_type !== "all"
? `SELECT pd.doc_type, pd.title, pd.source_url, pd.file_size_bytes, pd.page_count
FROM product_documents pd
WHERE pd.switch_id = $1 AND pd.doc_type = $2
ORDER BY pd.doc_type, pd.title`
: `SELECT pd.doc_type, pd.title, pd.source_url, pd.file_size_bytes, pd.page_count
FROM product_documents pd
WHERE pd.switch_id = $1
ORDER BY pd.doc_type, pd.title`;
const docParams = doc_type !== "all" ? [sw.id, doc_type] : [sw.id];
const docsResult = await pool.query(docQuery, docParams);
let text = `## ${sw.vendor_name} ${sw.model} (${sw.series})\n\n`;
if (sw.product_page_url) {
text += `**Product Page:** ${sw.product_page_url}\n`;
}
if (sw.image_url) {
text += `**Product Image:** ${sw.image_url}\n`;
}
if (sw.datasheet_url) {
text += `**Datasheet:** ${sw.datasheet_url}\n`;
}
if (sw.vendor_website) {
text += `**Vendor Website:** ${sw.vendor_website}\n`;
}
if (docsResult.rows.length > 0) {
text += `\n### Documents (${docsResult.rows.length})\n\n`;
for (const doc of docsResult.rows) {
const size = doc.file_size_bytes ? ` (${(doc.file_size_bytes / 1024 / 1024).toFixed(1)} MB)` : "";
const pages = doc.page_count ? `, ${doc.page_count} pages` : "";
text += `- **[${doc.doc_type}]** ${doc.title}${size}${pages}\n ${doc.source_url}\n`;
}
} else {
text += "\nNo downloaded documents yet. Run `tsx src/index.ts --switch-assets` to fetch them.\n";
}
results.push(text);
}
return {
content: [{ type: "text", text: results.join("\n---\n\n") }],
};
}
);
// --- Tool: search_switches ---
server.tool(
"search_switches",
"Search switches and routers by specs, vendor, or category. Returns matching devices with their transceiver port configuration.",
{
query: z.string().optional().describe("Free text query, e.g. 'Cisco 400G spine' or 'industrial Hirschmann'"),
vendor: z.string().optional().describe("Vendor name filter"),
category: z.enum(["DataCenter", "Campus", "Edge", "Core", "SP", "Industrial"]).optional(),
min_speed_gbps: z.number().optional().describe("Minimum port speed in Gbps"),
max_results: z.number().default(10),
},
async ({ query, vendor, category, min_speed_gbps, max_results }) => {
const conditions: string[] = [];
const values: unknown[] = [];
let idx = 1;
if (query) {
conditions.push(`sw.search_vector @@ plainto_tsquery('english', $${idx})`);
values.push(query);
idx++;
}
if (vendor) {
conditions.push(`v.name ILIKE $${idx}`);
values.push(`%${vendor}%`);
idx++;
}
if (category) {
conditions.push(`sw.category = $${idx}`);
values.push(category);
idx++;
}
if (min_speed_gbps) {
conditions.push(`sw.max_speed_gbps >= $${idx}`);
values.push(min_speed_gbps);
idx++;
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const orderBy = query
? `ORDER BY ts_rank(sw.search_vector, plainto_tsquery('english', $1)) DESC`
: `ORDER BY sw.max_speed_gbps DESC NULLS LAST`;
const result = await pool.query(
`SELECT sw.id, sw.model, sw.series, sw.category, sw.layer,
sw.ports_config, sw.total_ports, sw.max_speed_gbps,
sw.switching_capacity_tbps, sw.asic_vendor, sw.asic_model,
sw.image_url, sw.datasheet_url, sw.product_page_url,
v.name as vendor_name
FROM switches sw
LEFT JOIN vendors v ON sw.vendor_id = v.id
${where}
${orderBy}
LIMIT ${max_results}`,
values
);
if (result.rows.length === 0) {
return {
content: [{ type: "text", text: "No switches found matching your criteria." }],
};
}
const lines = result.rows.map((sw) => {
const ports = typeof sw.ports_config === "string" ? JSON.parse(sw.ports_config) : sw.ports_config;
const portStr = Object.entries(ports || {}).map(([k, v]) => `${v}x ${k.replace(/_/g, " ")}`).join(", ");
let text = `**${sw.vendor_name} ${sw.model}** (${sw.series}) — ${sw.category} ${sw.layer}\n`;
text += ` Ports: ${portStr || "N/A"} | Max: ${sw.max_speed_gbps}G`;
if (sw.switching_capacity_tbps) text += ` | Capacity: ${sw.switching_capacity_tbps}Tbps`;
if (sw.asic_vendor) text += ` | ASIC: ${sw.asic_vendor} ${sw.asic_model || ""}`;
if (sw.image_url) text += `\n Image: ${sw.image_url}`;
if (sw.datasheet_url) text += `\n Datasheet: ${sw.datasheet_url}`;
return text;
});
return {
content: [{ type: "text", text: `Found ${result.rows.length} switches:\n\n${lines.join("\n\n")}` }],
};
}
);
}