#!/usr/bin/env node /** * TIP MCP Server — Transceiver Intelligence Platform * * 15 Tools for LLM access to transceiver data, pricing, compatibility, * hype cycle, knowledge base, news, market intelligence, and blog generation. * * Transport: stdio (for Claude Code, EO Global Pulse, etc.) * * Usage: * tsx src/index.ts — Run MCP server via stdio * npx @tip/mcp-server — After npm install -g * * Claude Code config (~/.claude/mcp.json): * { "tip": { "command": "npx", "args": ["@tip/mcp-server"] } } */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { pool } from "./db.js"; import { registerPricingTools } from "./tools/pricing.js"; import { registerCompatibilityTools } from "./tools/compatibility.js"; import { registerKnowledgeTools } from "./tools/knowledge.js"; import { registerContentTools } from "./tools/content.js"; import { registerMarketTools } from "./tools/market.js"; import { registerSwitchDocTools } from "./tools/switch-docs.js"; import { finderTools, handleFinderTool } from "./tools/finder.js"; async function main() { const server = new McpServer({ name: "tip-mcp-server", version: "0.2.0", }); // --- Tool: search_transceivers --- server.tool( "search_transceivers", "Search transceivers by free text, specs, or compatibility. Returns matching transceivers with current pricing if available.", { query: z.string().optional().describe("Free text query, e.g. '10km for Cisco Nexus' or '400G QSFP-DD ZR'"), form_factor: z.string().optional().describe("SFP, SFP+, SFP28, QSFP+, QSFP28, QSFP-DD, OSFP, CFP2, etc."), speed_gbps: z.number().optional().describe("Speed in Gbps: 1, 10, 25, 40, 100, 200, 400, 800"), reach_label: z.string().optional().describe("SR, LR, ER, ZR, or distance like 10km, 80km"), fiber_type: z.enum(["SMF", "MMF"]).optional().describe("Single-mode or Multi-mode fiber"), wdm_type: z.enum(["CWDM", "DWDM"]).optional().describe("Wavelength division multiplexing type"), vendor: z.string().optional().describe("Vendor/manufacturer filter, e.g. 'Cisco', 'Juniper', 'FS.COM', 'Flexoptix'"), category: z.string().optional().describe("Category filter: DataCenter, AOC, DAC, DWDM, CWDM, Coherent, Metro, LongHaul, etc."), market_status: z.enum(["Mainstream", "Growth", "Emerging", "Legacy", "EOL"]).optional().describe("Market status filter"), max_results: z.number().default(10).describe("Maximum results to return"), }, async ({ query, form_factor, speed_gbps, reach_label, fiber_type, wdm_type, vendor, category, market_status, max_results }) => { const conditions: string[] = []; const values: unknown[] = []; let idx = 1; if (query) { conditions.push(`t.search_vector @@ plainto_tsquery('english', $${idx})`); values.push(query); idx++; } if (form_factor) { conditions.push(`t.form_factor ILIKE $${idx}`); values.push(`%${form_factor}%`); idx++; } if (speed_gbps) { conditions.push(`t.speed_gbps = $${idx}`); values.push(speed_gbps); idx++; } if (reach_label) { conditions.push(`(t.reach_label ILIKE $${idx} OR t.standard_name ILIKE $${idx})`); values.push(`%${reach_label}%`); idx++; } if (fiber_type) { conditions.push(`t.fiber_type = $${idx}`); values.push(fiber_type); idx++; } if (wdm_type) { conditions.push(`t.wdm_type = $${idx}`); values.push(wdm_type); idx++; } if (vendor) { conditions.push(`v.name ILIKE $${idx}`); values.push(`%${vendor}%`); idx++; } if (category) { conditions.push(`t.category ILIKE $${idx}`); values.push(`%${category}%`); idx++; } if (market_status) { conditions.push(`t.market_status = $${idx}`); values.push(market_status); idx++; } const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const orderBy = query ? `ORDER BY ts_rank(t.search_vector, plainto_tsquery('english', $1)) DESC` : "ORDER BY t.speed_gbps DESC, t.reach_meters ASC"; values.push(max_results); const result = await pool.query( `SELECT t.id, t.slug, t.standard_name, t.form_factor, t.speed, t.speed_gbps, t.reach_label, t.reach_meters, t.fiber_type, t.connector, t.wdm_type, t.wavelengths, t.power_consumption_w, t.temp_range, t.category, t.market_status, t.hype_cycle_phase, v.name as vendor_name, (SELECT jsonb_agg(jsonb_build_object( 'vendor', sv.name, 'price', po.price, 'currency', po.currency, 'stock', po.stock_level, 'url', po.url ) ORDER BY po.time DESC) FROM price_observations po JOIN vendors sv ON sv.id = po.source_vendor_id WHERE po.transceiver_id = t.id AND po.time > NOW() - INTERVAL '7 days' ) as pricing FROM transceivers t LEFT JOIN vendors v ON v.id = t.vendor_id ${where} ${orderBy} LIMIT $${idx}`, values ); if (result.rows.length === 0) { return { content: [{ type: "text", text: "No transceivers found matching your criteria." }], }; } const formatted = result.rows.map((r) => ({ slug: r.slug, standard: r.standard_name, form_factor: r.form_factor, speed: r.speed, speed_gbps: r.speed_gbps, reach: r.reach_label, fiber: r.fiber_type, connector: r.connector, wdm: r.wdm_type, wavelengths: r.wavelengths, power_w: r.power_consumption_w, temp: r.temp_range, category: r.category, market_status: r.market_status, vendor: r.vendor_name, pricing: r.pricing || [], })); return { content: [{ type: "text", text: JSON.stringify({ count: result.rows.length, transceivers: formatted }, null, 2), }], }; } ); // --- Tool: check_compatibility --- server.tool( "check_compatibility", "Check compatibility between a switch model and transceivers. Returns verified compatible transceivers with firmware requirements. When no exact match is found, suggests alternative transceivers that may work.", { switch_model: z.string().describe("Switch model, e.g. 'Cisco Nexus 93180YC-FX3' or 'Juniper EX4300'"), transceiver_query: z.string().optional().describe("Optional: filter by transceiver type or part number"), speed_gbps: z.number().optional().describe("Optional: filter by speed"), reach: z.string().optional().describe("Optional: filter by reach (SR, LR, etc.)"), }, async ({ switch_model, transceiver_query, speed_gbps, reach }) => { // First: find the switch const switchResult = await pool.query( `SELECT s.id, s.model, s.series, s.max_speed_gbps, s.ports_config, v.name as vendor FROM switches s JOIN vendors v ON v.id = s.vendor_id WHERE s.model ILIKE $1 OR s.series ILIKE $1 LIMIT 5`, [`%${switch_model}%`] ); if (switchResult.rows.length === 0) { // Suggest similar switches using trigram similarity const similarResult = await pool.query( `SELECT s.model, s.series, v.name as vendor, similarity(s.model, $1) as sim FROM switches s JOIN vendors v ON v.id = s.vendor_id WHERE similarity(s.model, $1) > 0.1 ORDER BY sim DESC LIMIT 5`, [switch_model] ); const suggestions = similarResult.rows.length > 0 ? `\n\nDid you mean one of these?\n${similarResult.rows.map(r => ` - ${r.vendor} ${r.model} (${r.series})`).join("\n")}` : ""; return { content: [{ type: "text", text: `No switch found matching "${switch_model}". Try a shorter model name or check spelling.${suggestions}`, }], }; } const sw = switchResult.rows[0]; const conditions = [`c.switch_id = $1`]; const values: unknown[] = [sw.id]; let idx = 2; if (transceiver_query) { conditions.push(`(t.standard_name ILIKE $${idx} OR t.slug ILIKE $${idx})`); values.push(`%${transceiver_query}%`); idx++; } if (speed_gbps) { conditions.push(`t.speed_gbps = $${idx}`); values.push(speed_gbps); idx++; } if (reach) { conditions.push(`t.reach_label ILIKE $${idx}`); values.push(`%${reach}%`); idx++; } const compatResult = await pool.query( `SELECT t.id, t.slug, t.standard_name, t.form_factor, t.speed, t.speed_gbps, t.reach_label, t.reach_meters, t.fiber_type, c.status, c.firmware_min, c.verified_by, c.verification_method, c.notes as compat_notes, (SELECT MIN(po.price) FROM price_observations po WHERE po.transceiver_id = t.id AND po.time > NOW() - INTERVAL '7 days' ) as min_price FROM compatibility c JOIN transceivers t ON t.id = c.transceiver_id WHERE ${conditions.join(" AND ")} AND c.status = 'compatible' ORDER BY t.speed_gbps DESC, t.reach_meters ASC LIMIT 20`, values ); // If no compatible transceivers found, suggest alternatives let alternatives: unknown[] = []; if (compatResult.rows.length === 0) { // Find what form factors / speeds this switch supports based on ports_config const portSpeeds: number[] = []; if (sw.max_speed_gbps) portSpeeds.push(parseFloat(sw.max_speed_gbps)); // Try to find transceivers that match the requested criteria even without verified compatibility const altConditions: string[] = []; const altValues: unknown[] = []; let altIdx = 1; if (transceiver_query) { altConditions.push(`(t.standard_name ILIKE $${altIdx} OR t.slug ILIKE $${altIdx})`); altValues.push(`%${transceiver_query}%`); altIdx++; } if (speed_gbps) { altConditions.push(`t.speed_gbps = $${altIdx}`); altValues.push(speed_gbps); altIdx++; } if (reach) { altConditions.push(`t.reach_label ILIKE $${altIdx}`); altValues.push(`%${reach}%`); altIdx++; } const altWhere = altConditions.length > 0 ? `WHERE ${altConditions.join(" AND ")}` : ""; const altResult = await pool.query( `SELECT t.slug, t.standard_name, t.form_factor, t.speed, t.speed_gbps, t.reach_label, t.fiber_type, v.name as vendor, (SELECT MIN(po.price) FROM price_observations po WHERE po.transceiver_id = t.id AND po.time > NOW() - INTERVAL '7 days' ) as min_price, -- Check if this transceiver is compatible with OTHER switches from the same vendor (SELECT COUNT(*) FROM compatibility c2 JOIN switches sw2 ON sw2.id = c2.switch_id WHERE c2.transceiver_id = t.id AND c2.status = 'compatible' AND sw2.vendor_id = (SELECT vendor_id FROM switches WHERE id = '${sw.id}') ) as same_vendor_compat_count FROM transceivers t LEFT JOIN vendors v ON v.id = t.vendor_id ${altWhere} ORDER BY same_vendor_compat_count DESC, t.speed_gbps DESC LIMIT 10`, altValues ); alternatives = altResult.rows.map(r => ({ ...r, min_price: r.min_price ? parseFloat(r.min_price) : null, compatibility_note: parseInt(r.same_vendor_compat_count) > 0 ? `Compatible with ${r.same_vendor_compat_count} other ${sw.vendor} switches — likely compatible but NOT verified for ${sw.model}` : "Not verified for any switches from this vendor. Test before deploying.", })); } return { content: [{ type: "text", text: JSON.stringify({ switch: { model: sw.model, series: sw.series, vendor: sw.vendor, max_speed_gbps: sw.max_speed_gbps, }, compatible_transceivers: compatResult.rows.map(r => ({ slug: r.slug, standard: r.standard_name, form_factor: r.form_factor, speed: r.speed, reach: r.reach_label, fiber: r.fiber_type, status: r.status, firmware_min: r.firmware_min, verified_by: r.verified_by, method: r.verification_method, notes: r.compat_notes, min_price: r.min_price ? parseFloat(r.min_price) : null, })), count: compatResult.rows.length, ...(compatResult.rows.length === 0 && alternatives.length > 0 ? { no_verified_match: true, suggested_alternatives: alternatives, suggestion_note: `No verified compatible transceivers found for ${sw.vendor} ${sw.model}. These alternatives may work based on specs and compatibility with similar ${sw.vendor} switches, but should be tested before production deployment.`, } : {}), }, null, 2), }], }; } ); // Register tool modules await registerPricingTools(server); await registerCompatibilityTools(server); await registerKnowledgeTools(server); await registerContentTools(server); await registerMarketTools(server); await registerSwitchDocTools(server); // --- Register finder.ts tools (find_flexoptix_for_switch, get_competitor_alerts) --- for (const [toolName, toolDef] of Object.entries(finderTools)) { const schema: Record = {}; for (const [propName, propDef] of Object.entries(toolDef.inputSchema.properties ?? {})) { const p = propDef as { type: string; description?: string }; schema[propName] = p.type === "number" ? z.number().optional().describe(p.description ?? "") : z.string().optional().describe(p.description ?? ""); } server.tool( toolName, toolDef.description, schema, async (args) => { const result = await handleFinderTool(toolName, args as Record); return { content: [{ type: "text" as const, text: result }] }; } ); } // --- Ollama-compatible LLM tools: market analysis (TIP_LLM) + blog generation (FO_BlogLLM) --- const OLLAMA_BASE = process.env["OLLAMA_BASE_URL"] ?? "https://ollama.fichtmueller.org"; const TIP_LLM_MODEL = process.env["TIP_LLM_MODEL"] ?? "tip-llm-v1"; const BLOG_LLM_MODEL = process.env["BLOG_LLM_MODEL"] ?? "fo-blog-v7"; const BLOG_LLM_FALLBACK = process.env["BLOG_LLM_FALLBACK_MODEL"] ?? "qwen2.5:14b"; server.tool( "analyze_market_with_llm", "Deep market analysis for a transceiver technology using TIP_LLM. Provides expert narrative on adoption trends, pricing trajectory, competitive dynamics, and buy/wait/hold recommendation.", { technology: z.string().describe("Technology to analyze, e.g. '400G QSFP-DD', '800G OSFP', '100G ZR'"), context: z.string().optional().describe("Additional context or specific questions to address"), horizon: z.enum(["3m", "6m", "12m", "18m"]).default("12m").describe("Forecast horizon"), }, async ({ technology, context, horizon }) => { // Gather DB data to enrich the prompt const [hype, prices, news] = await Promise.all([ pool.query( `SELECT hype_phase, hype_score, ROUND(current_share*100,1) AS share_pct, asp_current_usd, asp_decline_pct_3y, years_to_next_phase FROM hype_cycle_analysis WHERE technology ILIKE $1 ORDER BY computed_at DESC LIMIT 1`, [`%${technology}%`] ), pool.query( `SELECT v.name AS vendor, ROUND(MIN(po.price)::NUMERIC,2) AS min_price, ROUND(MAX(po.price)::NUMERIC,2) AS max_price, po.currency FROM price_observations po JOIN vendors v ON v.id = po.source_vendor_id JOIN transceivers t ON t.id = po.transceiver_id WHERE t.speed ILIKE $1 AND po.time > NOW() - INTERVAL '7 days' GROUP BY v.name, po.currency ORDER BY min_price ASC LIMIT 10`, [`%${technology.split("-")[0]}%`] ), pool.query( `SELECT title, summary, published_at FROM news_articles WHERE content_vector @@ plainto_tsquery('english', $1) ORDER BY published_at DESC LIMIT 5`, [technology] ).catch(() => ({ rows: [] })), ]); const dataContext = [ hype.rows[0] ? `Hype Cycle: phase=${hype.rows[0].hype_phase}, score=${hype.rows[0].hype_score}/100, market_share=${hype.rows[0].share_pct}%, OEM_ASP=$${hype.rows[0].asp_current_usd}, ASP_decline_3y=${hype.rows[0].asp_decline_pct_3y}%, years_to_next_phase=${hype.rows[0].years_to_next_phase}` : "", prices.rows.length > 0 ? `Current pricing: ${prices.rows.map((r) => `${r.vendor} ${r.currency}${r.min_price}–${r.max_price}`).join(", ")}` : "", news.rows.length > 0 ? `Recent news: ${news.rows.map((r: {title:string}) => r.title).join(" | ")}` : "", ].filter(Boolean).join("\n"); const prompt = `You are a senior optical networking market analyst at a transceiver intelligence platform. Technology: ${technology} Forecast horizon: ${horizon} ${dataContext ? `\nLive data:\n${dataContext}` : ""} ${context ? `\nSpecific questions: ${context}` : ""} Provide a concise expert market analysis covering: 1. Current market phase and what it means for buyers/sellers 2. Price trajectory over the next ${horizon} — will prices rise, fall, or stabilize? 3. Key demand drivers and risks 4. Competitive dynamics (OEM vs compatible vendors) 5. Buy / Wait / Hold recommendation with reasoning Keep the analysis actionable and data-driven. Under 400 words.`; try { const resp = await fetch(`${OLLAMA_BASE}/api/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: TIP_LLM_MODEL, prompt, stream: false }), signal: AbortSignal.timeout(120_000), }); if (!resp.ok) throw new Error(`Ollama HTTP ${resp.status}`); const data = await resp.json() as { response?: string }; return { content: [{ type: "text" as const, text: data.response ?? "No response from model." }] }; } catch (err: unknown) { return { content: [{ type: "text" as const, text: `LLM unavailable: ${(err as Error).message}. Use /api/hype-cycle/analysis for raw data.` }] }; } } ); server.tool( "generate_blog_post", "Generate a professional Flexoptix blog post using the latest FO_BlogLLM model. Automatically enriched with live pricing, hype cycle data, and competitor analysis.", { topic: z.string().describe("Blog topic, e.g. '400G QSFP-DD vs 400G ZR — which for your DC?'"), target_audience: z.enum(["network_engineer", "procurement", "executive", "general"]).default("network_engineer").describe("Target reader"), tone: z.enum(["technical", "consultative", "educational"]).default("consultative").describe("Writing tone"), word_count: z.number().default(600).describe("Target word count (300–1000)"), }, async ({ topic, target_audience, tone, word_count }) => { // Gather enrichment data const keywords = topic.match(/\b(\d+G|QSFP|SFP|OSFP|ZR|SR|LR|ER)\b/gi) ?? []; const priceData = keywords.length > 0 ? await pool.query( `SELECT v.name AS vendor, t.form_factor, t.speed, ROUND(MIN(po.price)::NUMERIC,2) AS min_price, po.currency FROM price_observations po JOIN vendors v ON v.id = po.source_vendor_id JOIN transceivers t ON t.id = po.transceiver_id WHERE t.speed ILIKE ANY($1) AND po.time > NOW() - INTERVAL '7 days' GROUP BY v.name, t.form_factor, t.speed, po.currency ORDER BY min_price ASC LIMIT 8`, [keywords.map((k: string) => `%${k}%`)] ).catch(() => ({ rows: [] })) : { rows: [] }; const enrichment = priceData.rows.length > 0 ? `\nCurrent market prices (use naturally in article):\n${priceData.rows.map((r: {vendor:string;form_factor:string;speed:string;min_price:string;currency:string}) => `- ${r.form_factor} ${r.speed}: from ${r.currency}${r.min_price} at ${r.vendor}`).join("\n")}` : ""; const systemPrompt = `You are a professional technical writer for Flexoptix, Europe's leading transceiver specialist. Write in a ${tone} tone for a ${target_audience.replace(/_/g," ")} audience. Articles should highlight Flexoptix expertise and the value of our FlexBox universal coding solution.`; const userPrompt = `Write a ${word_count}-word blog post on: "${topic}" ${enrichment} Include: - Compelling introduction - Technical explanation appropriate for audience - Real pricing context where available - Call-to-action mentioning Flexoptix or FlexBox - SEO-friendly subheadings Do not include a title (added separately). Start directly with the article body.`; try { const resp = await fetch(`${OLLAMA_BASE}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: BLOG_LLM_MODEL, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, ], stream: false, }), signal: AbortSignal.timeout(180_000), }); if (!resp.ok) { // Fallback to generic local model if FO_BlogLLM is unavailable const fallbackResp = await fetch(`${OLLAMA_BASE}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: BLOG_LLM_FALLBACK, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, ], stream: false, }), signal: AbortSignal.timeout(180_000), }); if (!fallbackResp.ok) throw new Error(`Both ${BLOG_LLM_MODEL} and ${BLOG_LLM_FALLBACK} unavailable`); const fallbackData = await fallbackResp.json() as { message?: { content?: string } }; return { content: [{ type: "text" as const, text: `[Generated with ${BLOG_LLM_FALLBACK} — ${BLOG_LLM_MODEL} unavailable]\n\n${fallbackData.message?.content ?? "No content"}` }] }; } const data = await resp.json() as { message?: { content?: string } }; return { content: [{ type: "text" as const, text: data.message?.content ?? "No content generated." }] }; } catch (err: unknown) { return { content: [{ type: "text" as const, text: `LLM unavailable: ${(err as Error).message}. Check OLLAMA_BASE_URL env var.` }] }; } } ); // Start server const transport = new StdioServerTransport(); await server.connect(transport); // Graceful shutdown process.on("SIGINT", async () => { await pool.end(); process.exit(0); }); } main().catch((err) => { console.error("Fatal MCP server error:", err); process.exit(1); });