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.
167 lines
6.5 KiB
TypeScript
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")}` }],
|
|
};
|
|
}
|
|
);
|
|
}
|