Rene Fichtmueller 92f42832bf feat: Phase 2 — MCP Server with 12 tools
Implements all 12 MCP tools from CONCEPT document:
- search_transceivers: Full-text + spec filter search with pricing
- check_compatibility: Switch ↔ transceiver compatibility lookup
- get_pricing: Current prices + 30-day history across all vendors
- compare_prices: Multi-vendor price comparison with savings analysis
- get_competitor_stock: Live competitor stock monitoring (sales opportunities)
- suggest_alternatives: Similar spec alternatives optimized for price/availability
- get_templates: FlexBox coding and switch config template finder
- search_knowledge_base: Troubleshooting FAQ search (PostgreSQL full-text)
- search_manuals: Switch manual and datasheet search
- get_hype_cycle: Norton-Bass adoption forecast + Gartner phase classification
- get_market_news: Aggregated news with relevance scoring
- generate_blog_draft: Data-driven blog drafts saved to blog_drafts table

Transport: stdio (MCP protocol 2024-11-05)
Config: .mcp.json for Claude Code integration
Verified: all 12 tools registered, search_transceivers returns DB results
2026-03-27 16:48:34 +13:00

234 lines
9.2 KiB
TypeScript

/**
* Content tools: get_market_news, generate_blog_draft
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { pool } from "../db.js";
export async function registerContentTools(server: McpServer): Promise<void> {
// --- Tool: get_market_news ---
server.tool(
"get_market_news",
"Get latest news from the optics and networking industry. Filtered by relevance to transceiver technology.",
{
query: z.string().optional().describe("Optional: search within news content"),
event: z.enum(["OFC", "ECOC", "CIOE", "PHOTONICS_WEST"]).optional().describe("Filter by trade show/event"),
days_back: z.number().default(30).describe("How many days back to look (default: 30)"),
min_relevance: z.number().default(0).describe("Minimum relevance score (0=all, 3=transceiver-related, 9=highly relevant)"),
},
async ({ query, event, days_back, min_relevance }) => {
const conditions = [`na.published_at > NOW() - INTERVAL '${days_back} days'`];
const values: unknown[] = [];
let idx = 1;
if (query) {
conditions.push(`(na.search_vector @@ plainto_tsquery('english', $${idx}) OR na.title ILIKE $${idx + 1})`);
values.push(query, `%${query}%`);
idx += 2;
}
if (event) {
conditions.push(`na.event = $${idx}`);
values.push(event);
idx++;
}
if (min_relevance > 0) {
conditions.push(`na.relevance_score >= $${idx}`);
values.push(min_relevance);
idx++;
}
const result = await pool.query(
`SELECT na.title, na.source, na.published_at, na.summary,
na.source_url, na.relevance_score, na.event,
na.mentioned_vendors, na.mentioned_products
FROM news_articles na
WHERE ${conditions.join(" AND ")}
ORDER BY na.relevance_score DESC, na.published_at DESC
LIMIT 20`,
values
);
const bySource: Record<string, number> = {};
for (const row of result.rows) {
bySource[row.source] = (bySource[row.source] || 0) + 1;
}
return {
content: [{
type: "text",
text: JSON.stringify({
articles: result.rows,
count: result.rows.length,
sources: bySource,
period: `Last ${days_back} days`,
}, null, 2),
}],
};
}
);
// --- Tool: generate_blog_draft ---
server.tool(
"generate_blog_draft",
"Generate a blog post draft based on market data, price trends, hype cycle position, and recent news. Saved to blog_drafts table for review.",
{
topic: z.enum(["hype_cycle", "price_trend", "new_product", "comparison", "tutorial"]),
technology: z.string().optional().describe("Technology to focus on, e.g. '800G OSFP', 'DWDM', '400G'"),
target_audience: z.enum(["sales", "technical", "customer", "seo"]).default("technical"),
},
async ({ topic, technology, target_audience }) => {
// Gather data for the blog post
const data: Record<string, unknown> = { topic, technology, target_audience };
// Get relevant transceivers
if (technology) {
const txResult = await pool.query(
`SELECT t.standard_name, t.form_factor, t.speed, t.reach_label,
t.fiber_type, t.category,
(SELECT MIN(po.price) FROM price_observations po
WHERE po.transceiver_id = t.id
AND po.time > NOW() - INTERVAL '7 days') as min_price,
(SELECT MAX(po.price) FROM price_observations po
WHERE po.transceiver_id = t.id
AND po.time > NOW() - INTERVAL '7 days') as max_price
FROM transceivers t
WHERE t.standard_name ILIKE $1 OR t.speed ILIKE $1
LIMIT 10`,
[`%${technology}%`]
);
data.transceivers = txResult.rows;
}
// Get recent news
const newsResult = await pool.query(
`SELECT title, source, published_at, summary
FROM news_articles
WHERE ($1 IS NULL OR title ILIKE $1 OR summary ILIKE $1)
ORDER BY relevance_score DESC, published_at DESC
LIMIT 5`,
[technology ? `%${technology}%` : null]
);
data.recent_news = newsResult.rows;
// Get price trends
const priceResult = await pool.query(
`SELECT t.standard_name, t.speed,
AVG(po.price) as avg_price,
MIN(po.price) as min_price,
COUNT(DISTINCT po.source_vendor_id) as vendor_count
FROM price_observations po
JOIN transceivers t ON t.id = po.transceiver_id
WHERE ($1 IS NULL OR t.standard_name ILIKE $1 OR t.speed ILIKE $1)
AND po.time > NOW() - INTERVAL '30 days'
GROUP BY t.id, t.standard_name, t.speed
ORDER BY vendor_count DESC
LIMIT 10`,
[technology ? `%${technology}%` : null]
);
data.price_trends = priceResult.rows;
// Generate blog outline based on topic and audience
const outlines: Record<string, Record<string, string[]>> = {
hype_cycle: {
sales: [
`## Where is ${technology || "The Market"} on the Hype Cycle?`,
"## What This Means for Your Customers",
"## When to Buy: Timing the Market",
"## Flexoptix Recommendation",
],
technical: [
`## ${technology || "Technology"} Market Analysis — Norton-Bass Diffusion Model`,
"## Current Phase: Technical Readiness",
"## Vendor Ecosystem Status",
"## Price Trajectory & ASP Forecast",
"## Deployment Considerations",
],
customer: [
`## Is ${technology || "This Technology"} Right for You?`,
"## Cost vs. Performance Analysis",
"## Compatibility & Migration Path",
"## When Will Prices Drop?",
],
seo: [
`## ${technology || "Optical Transceiver"} Market 2026: Complete Guide`,
"## Best Vendors & Pricing Comparison",
"## Compatibility Guide",
"## FAQ",
],
},
price_trend: {
sales: ["## Price Alert", "## Competitor Pricing Analysis", "## Sales Opportunity"],
technical: ["## Price Trend Analysis", "## ASP History", "## Market Drivers"],
customer: ["## How Much Should You Pay?", "## Price Forecast", "## When to Buy"],
seo: ["## Price Guide 2026", "## Best Deals", "## Comparison Table"],
},
comparison: {
sales: ["## Why Flexoptix Beats the Competition", "## Price Advantage", "## Quality & Compatibility"],
technical: ["## Vendor Comparison", "## Spec Analysis", "## Performance Benchmarks"],
customer: ["## OEM vs. Compatible: The Facts", "## Risk Analysis", "## Cost Savings"],
seo: ["## Best Transceiver Vendors 2026", "## Comparison Table", "## Reviews"],
},
tutorial: {
technical: ["## Prerequisites", "## Step-by-Step Configuration", "## Troubleshooting", "## Verification"],
customer: ["## Getting Started", "## Installation Guide", "## Tips & Tricks"],
sales: ["## Product Overview", "## Use Cases", "## Getting Support"],
seo: ["## How To Guide", "## Step-by-Step", "## FAQ"],
},
new_product: {
sales: ["## New Product Alert", "## What's New", "## Pricing & Availability"],
technical: ["## Technical Specs", "## Compatibility Matrix", "## Performance Data"],
customer: ["## What You Get", "## Why You Need It", "## How to Order"],
seo: ["## Product Announcement", "## Specs & Features", "## Where to Buy"],
},
};
const outline = outlines[topic]?.[target_audience] || [];
// Build draft content
const draft = {
title: `${technology || "Optical Transceiver"} ${topic.replace(/_/g, " ")}${new Date().getFullYear()} Analysis`,
topic,
technology,
target_audience,
outline,
data_points: data,
generation_note: "This is a data-driven draft. Review and enrich with specific product details before publishing.",
generated_at: new Date().toISOString(),
status: "draft",
};
// Save to blog_drafts table
await pool.query(
`INSERT INTO blog_drafts (title, topic, technology, target_audience, outline, draft_content, status)
VALUES ($1, $2, $3, $4, $5, $6, 'draft')
ON CONFLICT DO NOTHING`,
[
draft.title,
topic,
technology || null,
target_audience,
JSON.stringify(outline),
JSON.stringify(draft),
]
);
return {
content: [{
type: "text",
text: JSON.stringify({
draft,
saved_to_database: true,
next_steps: [
"Review the outline and data points",
"Enrich with specific product examples from search_transceivers",
"Add competitor pricing from compare_prices",
"Include current news context from get_market_news",
"Submit to content team for writing/editing",
],
}, null, 2),
}],
};
}
);
}