#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const toneInstructions: Record<string, string> = {
professional: "Use a professional, authoritative tone. Focus on quality, reliability, and expertise.",
casual: "Use a friendly, conversational tone. Be approachable and relatable.",
luxury: "Use an elegant, sophisticated tone. Emphasize exclusivity, craftsmanship, and premium quality.",
playful: "Use a fun, energetic tone. Be creative and engaging with a touch of humor.",
minimal: "Use a clean, minimalist tone. Be concise and let the product speak for itself.",
};
const platformInstructions: Record<string, string> = {
shopify:
"Write for a Shopify product page. Use an engaging, brand-friendly tone. Include HTML-ready formatting with short paragraphs. Focus on lifestyle benefits and features.",
amazon:
"Write for an Amazon product listing. Follow Amazon's style: concise, feature-driven, benefit-oriented bullet points. Use persuasive but factual language. Include relevant search terms naturally.",
etsy:
"Write for an Etsy product listing. Use a warm, artisan, handmade-friendly tone. Emphasize craftsmanship, uniqueness, and story. Appeal to Etsy's audience who values authenticity.",
ebay:
"Write for an eBay listing. Be clear, factual, and detail-oriented. Include specifications and condition details. Appeal to value-conscious buyers.",
woocommerce:
"Write for a WooCommerce store. Professional yet accessible, with clear feature highlights and SEO-friendly content.",
general:
"Write for a general e-commerce product page. Use a professional, conversion-focused tone. Balance features with benefits.",
};
interface GenerateParams {
product_name: string;
features: string;
tone?: string;
platform?: string;
}
interface GeneratedCopy {
description: string;
bullets: string[];
metaTitle: string;
metaDescription: string;
keywords: string[];
wordCount: number;
}
async function generateProductCopy(params: GenerateParams): Promise<GeneratedCopy> {
const { product_name, features, tone = "professional", platform = "general" } = params;
const toneGuide = toneInstructions[tone] || toneInstructions.professional;
const platformGuide = platformInstructions[platform] || platformInstructions.general;
const prompt = `You are an expert e-commerce copywriter. Generate a product listing for the following product.
PRODUCT: ${product_name}
FEATURES: ${features}
PLATFORM: ${platform}
PLATFORM GUIDELINES: ${platformGuide}
TONE: ${toneGuide}
Generate the following in JSON format:
{
"description": "A compelling product description (150-250 words). Write in short paragraphs, highlighting benefits and features. Make it conversion-focused and SEO-friendly.",
"bullets": ["Array of 5 key benefit/feature bullet points. Each should be concise (under 20 words) and start with a strong action word or benefit."],
"metaTitle": "SEO meta title (50-60 characters) that includes the product name and key selling point",
"metaDescription": "SEO meta description (140-160 characters) that summarizes the product and includes a call to action",
"keywords": ["Array of 6-8 relevant SEO keywords/phrases for this product"]
}
Return ONLY valid JSON, no markdown code blocks or other text.`;
const message = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
const textContent = message.content.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text") {
throw new Error("No text response from AI");
}
let jsonStr = textContent.text.trim();
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
if (jsonMatch) {
jsonStr = jsonMatch[0];
}
const parsed = JSON.parse(jsonStr);
const allText = `${parsed.description} ${parsed.bullets.join(" ")}`;
const wordCount = allText.split(/\s+/).filter(Boolean).length;
return {
description: parsed.description,
bullets: parsed.bullets,
metaTitle: parsed.metaTitle,
metaDescription: parsed.metaDescription,
keywords: parsed.keywords,
wordCount,
};
}
const server = new Server(
{
name: "copyforge-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "generate_product_copy",
description:
"Generate compelling e-commerce product copy including description, bullet points, SEO metadata, and keywords. Powered by AI for conversion-optimized content.",
inputSchema: {
type: "object" as const,
properties: {
product_name: {
type: "string",
description: "The name of the product",
},
features: {
type: "string",
description:
"Key features and specifications of the product, comma-separated or as a description",
},
tone: {
type: "string",
description:
"Writing tone: professional, casual, luxury, playful, or minimal",
enum: ["professional", "casual", "luxury", "playful", "minimal"],
default: "professional",
},
platform: {
type: "string",
description:
"Target e-commerce platform: shopify, amazon, etsy, ebay, woocommerce, or general",
enum: ["shopify", "amazon", "etsy", "ebay", "woocommerce", "general"],
default: "general",
},
},
required: ["product_name", "features"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== "generate_product_copy") {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const args = request.params.arguments as unknown as GenerateParams;
if (!args.product_name || !args.features) {
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ error: "product_name and features are required" }),
},
],
isError: true,
};
}
try {
const result = await generateProductCopy(args);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : "Generation failed";
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ error: message }),
},
],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("CopyForge MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});