/** * 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 { // --- 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")}` }], }; } ); }