#!/usr/bin/env node /** * TIP MCP HTTP Server — SSE Transport * * Exposes all 12 TIP MCP tools over HTTP/SSE so the server can be registered * in Claude Code's ~/.mcp.json as a remote MCP server. * * Endpoints: * GET /health — Health check: { status: "ok", tools: 12 } * GET /sse — Opens SSE stream, returns sessionId in endpoint event * POST /message — Client-to-server messages (requires ?sessionId=...) * * Auth: * All endpoints (except /health) require: * Authorization: Bearer * * Config (env): * MCP_HTTP_PORT — Listening port (default: 3202) * MCP_SECRET — Bearer token for auth (required in production) * CORS_ORIGINS — Comma-separated allowed origins (default: localhost + 127.0.0.1) * * ~/.mcp.json entry: * { * "tip": { * "type": "sse", * "url": "http://localhost:3202/sse", * "headers": { "Authorization": "Bearer " } * } * } */ import express, { type Request, type Response, type NextFunction } from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.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 { registerSwitchDocTools } from "./tools/switch-docs.js"; import { registerMarketTools } from "./tools/market.js"; // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- const PORT = parseInt(process.env.MCP_HTTP_PORT ?? "3202", 10); const MCP_SECRET = process.env.MCP_SECRET ?? ""; const CORS_ORIGINS: string[] = [ "http://localhost", "http://127.0.0.1", ...(process.env.CORS_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean), ]; // --------------------------------------------------------------------------- // Tool count (keep in sync with index.ts tools + tool files) // search_transceivers, check_compatibility (index.ts) = 2 // pricing.ts: get_pricing, compare_prices, get_competitor_stock = 3 // compatibility.ts: suggest_alternatives, get_templates = 2 // knowledge.ts: search_knowledge_base, search_manuals, get_hype_cycle = 3 // content.ts: get_market_news, generate_blog_draft = 2 // switch-docs.ts: get_switch_docs, search_switches = 2 // market.ts: get_cable_recommendations, get_market_overview, get_technology_roadmap = 3 // Total = 17 registered // --------------------------------------------------------------------------- const TOOL_COUNT = 17; // --------------------------------------------------------------------------- // Build a new McpServer and register all tools (one server per SSE session) // --------------------------------------------------------------------------- async function createMcpServer(): Promise { const server = new McpServer({ name: "tip-mcp-server", version: "0.1.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 filter, e.g. 'Cisco', 'Juniper', 'FS.COM'"), max_results: z.number().default(10).describe("Maximum results to return"), }, async ({ query, form_factor, speed_gbps, reach_label, fiber_type, wdm_type, vendor, 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++; } 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, 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, 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, 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.", { 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 }) => { const switchResult = await pool.query( `SELECT s.id, s.model, s.series, 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) { return { content: [{ type: "text", text: `No switch found matching "${switch_model}". Try a shorter model name or check spelling.`, }], }; } 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.slug, t.standard_name, t.form_factor, t.speed, t.reach_label, t.fiber_type, c.status, c.firmware_min, c.verified_by, c.verification_method 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 ); return { content: [{ type: "text", text: JSON.stringify({ switch: { model: sw.model, series: sw.series, vendor: sw.vendor }, compatible_transceivers: compatResult.rows, count: compatResult.rows.length, }, null, 2), }], }; } ); // Register remaining tools from tool modules await registerPricingTools(server); await registerCompatibilityTools(server); await registerKnowledgeTools(server); await registerContentTools(server); await registerSwitchDocTools(server); await registerMarketTools(server); return server; } // --------------------------------------------------------------------------- // Auth middleware // --------------------------------------------------------------------------- function requireAuth(req: Request, res: Response, next: NextFunction): void { if (!MCP_SECRET) { // No secret configured — skip auth (development mode) next(); return; } const authHeader = req.headers["authorization"] ?? ""; const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : ""; if (token !== MCP_SECRET) { res.status(401).json({ error: "Unauthorized: invalid or missing bearer token" }); return; } next(); } // --------------------------------------------------------------------------- // CORS middleware // --------------------------------------------------------------------------- function applyCors(req: Request, res: Response, next: NextFunction): void { const origin = req.headers["origin"] ?? ""; const isAllowed = CORS_ORIGINS.some((allowed) => origin === allowed || origin.startsWith(allowed) ); if (isAllowed) { res.setHeader("Access-Control-Allow-Origin", origin); } res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); res.setHeader("Access-Control-Allow-Credentials", "true"); if (req.method === "OPTIONS") { res.sendStatus(204); return; } next(); } // --------------------------------------------------------------------------- // Session registry: sessionId → SSEServerTransport // --------------------------------------------------------------------------- const sessions = new Map(); // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main(): Promise { const app = express(); app.use(express.json()); app.use(applyCors); // --- GET /health --- app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok", tools: TOOL_COUNT }); }); // --- GET /sse --- open SSE stream app.get("/sse", requireAuth, async (req: Request, res: Response) => { const transport = new SSEServerTransport("/message", res); // Register session before starting so POST /message can find it immediately sessions.set(transport.sessionId, transport); transport.onclose = () => { sessions.delete(transport.sessionId); }; // Each SSE connection gets its own McpServer instance const server = await createMcpServer(); await server.connect(transport); // Propagate close event from request disconnect req.on("close", () => { transport.close().catch(() => { // ignore errors on close }); }); }); // --- POST /message --- receive client messages app.post("/message", requireAuth, async (req: Request, res: Response) => { const sessionId = req.query["sessionId"] as string | undefined; if (!sessionId) { res.status(400).json({ error: "Missing required query parameter: sessionId" }); return; } const transport = sessions.get(sessionId); if (!transport) { res.status(404).json({ error: `No active SSE session for sessionId: ${sessionId}` }); return; } await transport.handlePostMessage(req, res, req.body); }); const httpServer = app.listen(PORT, () => { console.log(`TIP MCP HTTP server listening on port ${PORT}`); console.log(` SSE endpoint: http://localhost:${PORT}/sse`); console.log(` Message endpoint: http://localhost:${PORT}/message`); console.log(` Health endpoint: http://localhost:${PORT}/health`); if (!MCP_SECRET) { console.warn(" WARNING: MCP_SECRET is not set — auth is disabled (development mode only)"); } }); // Graceful shutdown process.on("SIGINT", async () => { for (const transport of sessions.values()) { await transport.close().catch(() => { // ignore errors on close }); } sessions.clear(); await pool.end(); httpServer.close(() => { process.exit(0); }); }); } main().catch((err: unknown) => { console.error("Fatal TIP MCP HTTP server error:", err); process.exit(1); });