@mayurgodhani/ecomtools-cli
Version:
E-commerce tools MCP server for Shopify development
287 lines (248 loc) • 8.63 kB
JavaScript
/**
* E-commerce Tools MCP Server
* Server implementation for ecomTools Model Context Protocol
*/
const { McpServer, ResourceTemplate } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const { z } = require("zod");
const fs = require('fs');
const path = require('path');
const { parseCodeFile, loadCodeFiles, findMatchingSnippet } = require('./utils/files');
// Create and initialize the MCP server
async function createServer() {
// Get data directory path, allowing for both local development and installed package
const dataDir = fs.existsSync(path.join(__dirname, '../data'))
? path.join(__dirname, '../data')
: path.join(__dirname, '../node_modules/ecomtools-cli/data');
console.log(`Loading code snippets from ${dataDir}`);
// Ensure the data directory exists
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// Load code snippets from the data directory
const codeSnippets = await loadCodeFiles(dataDir);
console.log(`Loaded ${codeSnippets.length} code snippets`);
const server = new McpServer({
name: "ecomTools",
version: "1.0.0",
description: "E-commerce tools and utilities for Shopify development"
});
// ----------- TOOLS -----------
// Calculate discount price
server.tool(
"calculateDiscount",
{
price: z.number().describe("Original price"),
discountPercent: z.number().describe("Discount percentage (0-100)")
},
async ({ price, discountPercent }) => {
const discountAmount = price * (discountPercent / 100);
const finalPrice = price - discountAmount;
return {
content: [{
type: "text",
text: `Original price: $${price.toFixed(2)}\nDiscount: ${discountPercent}% ($${discountAmount.toFixed(2)})\nFinal price: $${finalPrice.toFixed(2)}`
}]
};
}
);
// Generate product JSON-LD schema
server.tool(
"generateProductSchema",
{
name: z.string().describe("Product name"),
description: z.string().describe("Product description"),
price: z.number().describe("Product price"),
currency: z.string().default("USD").describe("Currency code"),
image: z.string().optional().describe("Product image URL")
},
async ({ name, description, price, currency, image }) => {
const schema = {
"@context": "https://schema.org/",
"@type": "Product",
"name": name,
"description": description,
"offers": {
"@type": "Offer",
"price": price,
"priceCurrency": currency
}
};
if (image) {
schema.image = image;
}
return {
content: [{
type: "text",
text: `<script type="application/ld+json">\n${JSON.stringify(schema, null, 2)}\n</script>`
}]
};
}
);
// Find code snippets
server.tool(
"findCodeSnippet",
{
keywords: z.string().describe("Keywords to search for in the code snippets")
},
async ({ keywords }) => {
const snippet = findMatchingSnippet(codeSnippets, keywords);
if (snippet) {
return {
content: [{
type: "text",
text: `# ${snippet.title}\n\n${snippet.description}\n\n\`\`\`\n${snippet.code}\n\`\`\``
}]
};
} else {
return {
content: [{
type: "text",
text: `No code snippets found matching "${keywords}". Try different keywords or check available code snippets.`
}]
};
}
}
);
// Generate code
server.tool(
"generateCode",
{
type: z.string().describe("Type of code to generate (e.g., cart-drawer, product-quick-view)"),
requirements: z.string().describe("Custom requirements or modifications for the code")
},
async ({ type, requirements }) => {
// Load snippet based on type
const typeKeyword = type.toLowerCase().replace(/[^a-z0-9]/g, '-');
const snippet = findMatchingSnippet(codeSnippets, typeKeyword);
if (!snippet) {
return {
content: [{
type: "text",
text: `No code template found for "${type}". Available types include: cart-drawer, product-card, etc.`
}]
};
}
// Return the code with a header showing the original type and requirements
return {
content: [{
type: "text",
text: `# Generated Shopify ${type} Code\n\nBased on your requirements: ${requirements}\n\n\`\`\`\n${snippet.code}\n\`\`\`\n\nTo customize this code based on your requirements:\n\n1. Follow the structure above\n2. Modify the styling to match your brand\n3. Adjust the functionality based on your specific needs`
}]
};
}
);
// Shopify Liquid cheatsheet
server.resource(
"liquid-cheatsheet",
"liquid://cheatsheet",
async (uri) => ({
contents: [{
uri: uri.href,
text: `# Shopify Liquid Cheatsheet
## Output
- {{ variable }} - Output a variable
- {{ variable | filter }} - Apply a filter to a variable
## Common Filters
- {{ "hello" | capitalize }} → "Hello"
- {{ "hello world" | split: " " }} → ["hello", "world"]
- {{ product.price | money }} → "$10.00"
- {{ "2022-01-01" | date: "%B %d, %Y" }} → "January 01, 2022"
## Control Flow
- {% if condition %}...{% endif %}
- {% if condition %}...{% else %}...{% endif %}
- {% if condition %}...{% elsif other_condition %}...{% endif %}
- {% unless condition %}...{% endunless %}
- {% case variable %}{% when value %}...{% endcase %}
## Loops
- {% for product in collection.products %}...{% endfor %}
- {% for i in (1..5) %}...{% endfor %}
- {% break %}
- {% continue %}
## Variables
- {% assign variable = value %}
- {% capture my_variable %}...{% endcapture %}
- {% increment variable %}
- {% decrement variable %}
## Schema Tags
- {% schema %}...{% endschema %}
- {% javascript %}...{% endjavascript %}
- {% stylesheet %}...{% endstylesheet %}`
}]
})
);
// Shopify theme structure guide
server.resource(
"theme-structure",
"theme://structure",
async (uri) => ({
contents: [{
uri: uri.href,
text: `# Shopify Theme Structure Guide
## Core Files & Directories
### Templates
The \`templates/\` directory contains Liquid templates for different page types:
- \`templates/index.liquid\` - Home page
- \`templates/product.liquid\` - Product pages
- \`templates/collection.liquid\` - Collection pages
- \`templates/cart.liquid\` - Cart page
- \`templates/blog.liquid\` - Blog listing page
- \`templates/article.liquid\` - Blog article page
- \`templates/page.liquid\` - Pages created in the admin
- \`templates/search.liquid\` - Search results page
- \`templates/customers/\` - Customer account pages
### Sections
The \`sections/\` directory contains modular components:
- \`sections/header.liquid\` - Site header
- \`sections/footer.liquid\` - Site footer
- \`sections/product-template.liquid\` - Product details
### Snippets
The \`snippets/\` directory contains reusable code snippets:
- \`snippets/product-card.liquid\` - Product grid item
### Assets
The \`assets/\` directory contains static files:
- \`assets/theme.js\` - JavaScript
- \`assets/theme.scss.liquid\` - Main stylesheet
- Images, fonts, and other static files
### Config
The \`config/\` directory contains:
- \`config/settings_schema.json\` - Theme settings schema
- \`config/settings_data.json\` - Theme settings data
### Layout
The \`layout/\` directory contains:
- \`layout/theme.liquid\` - Main layout template
### Locales
The \`locales/\` directory contains translation files:
- \`locales/en.default.json\` - English translations`
}]
})
);
return server;
}
// Connect to transport and start the server
async function startServer() {
try {
console.log('Starting ecomTools MCP server...');
const server = await createServer();
const transport = new StdioServerTransport();
console.log('Connecting to transport...');
await server.connect(transport);
console.log('Server connected and ready');
// Keep the process alive
process.on('SIGINT', () => {
console.log('Received SIGINT signal');
// Don't exit
});
process.on('SIGTERM', () => {
console.log('Received SIGTERM signal');
// Don't exit
});
} catch (err) {
console.error('Error starting server:', err);
process.exit(1);
}
}
module.exports = {
createServer,
startServer
};