UNPKG

@tsavo/printify-mcp

Version:

A Model Context Protocol (MCP) server for integrating AI assistants with Printify's print-on-demand platform

1,183 lines (1,180 loc) 50.7 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { PrintifyAPI } from "./printify-api.js"; import { ReplicateClient } from "./replicate-client.js"; import dotenv from "dotenv"; // File system imports moved to service modules // Export the main classes and types for use as a library export { PrintifyAPI } from './printify-api.js'; export { ReplicateClient } from './replicate-client.js'; export { createPrintifyMcpServer } from './exports.js'; // Export services for use as a library export * from './services/image-generator.js'; export * from './services/printify-uploader.js'; export * from './services/printify-products.js'; export * from './utils/error-handler.js'; export * from './utils/file-utils.js'; // Load environment variables from .env file dotenv.config(); // Create an MCP server const server = new McpServer({ name: "Printify-MCP", version: "1.0.0" }); // Initialize API clients let printifyClient = null; let replicateClient = null; // Note: The image conversion functionality has been moved to src/services/image-processor.ts // Auto-initialize the API clients when the server starts (async () => { try { // Initialize Printify API client const printifyApiKey = process.env.PRINTIFY_API_KEY; if (!printifyApiKey) { console.error("PRINTIFY_API_KEY environment variable is not set. The Printify API client will not be initialized."); } else { // Create the client with the API key printifyClient = new PrintifyAPI(printifyApiKey); // Initialize the client and fetch shops const shops = await printifyClient.initialize(); // Get the current shop after initialization const currentShop = printifyClient.getCurrentShop(); if (currentShop) { // A shop was automatically selected (either from PRINTIFY_SHOP_ID or the first available shop) console.log(`Printify SDK client initialized with shop: ${currentShop.title} (ID: ${currentShop.id})`); console.log(`Shop selection: ${process.env.PRINTIFY_SHOP_ID ? 'From environment variable' : 'Automatically selected first shop'}`); } else if (shops.length > 0) { // Shops exist but none was selected (this shouldn't happen with the current implementation) console.log(`Printify SDK client initialized, but no shop was selected. Available shops: ${shops.length}`); console.log('Attempting to select the first shop...'); printifyClient.setShopId(shops[0].id.toString()); console.log(`Selected shop: ${shops[0].title} (ID: ${shops[0].id})`); } else { console.log("Printify SDK client initialized, but no shops were found in your account."); console.log("Please make sure your Printify account has at least one shop."); } } // Initialize Replicate API client if environment variable is set const replicateApiToken = process.env.REPLICATE_API_TOKEN; if (!replicateApiToken) { console.error("REPLICATE_API_TOKEN environment variable is not set. The Replicate API client will not be initialized."); } else { replicateClient = new ReplicateClient(replicateApiToken); console.log('Replicate API client initialized successfully.'); } } catch (error) { console.error("Error initializing API clients:", error); } })(); // Get Printify status tool server.tool("get_printify_status", {}, async ({}) => { // Import the printify shops service const { getPrintifyStatus } = await import('./services/printify-shops.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await getPrintifyStatus(printifyClient); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // List shops tool server.tool("list_shops", {}, async ({}) => { // Import the printify shops service const { listPrintifyShops } = await import('./services/printify-shops.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await listPrintifyShops(printifyClient); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Switch shop tool server.tool("switch_shop", { shopId: z.string().describe("The ID of the shop to switch to") }, async ({ shopId }) => { // Import the printify shops service const { switchPrintifyShop } = await import('./services/printify-shops.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await switchPrintifyShop(printifyClient, shopId); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // This tool is now replaced by the list_shops tool // List products tool server.tool("list_products", { page: z.number().optional().default(1).describe("Page number"), limit: z.number().optional().default(10).describe("Number of products per page") }, async ({ page, limit }) => { // Import the printify products service const { listProducts } = await import('./services/printify-products.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await listProducts(printifyClient, { page, limit }); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Get product tool server.tool("get_product", { productId: z.string().describe("Product ID") }, async ({ productId }) => { // Import the printify products service const { getProduct } = await import('./services/printify-products.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await getProduct(printifyClient, productId); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Create product tool server.tool("create_product", { title: z.string().describe("Product title"), description: z.string().describe("Product description"), blueprintId: z.number().describe("Blueprint ID"), printProviderId: z.number().describe("Print provider ID"), variants: z.array(z.object({ variantId: z.number().describe("Variant ID"), price: z.number().describe("Price in cents (e.g., 1999 for $19.99)"), isEnabled: z.boolean().optional().default(true).describe("Whether the variant is enabled") })).describe("Product variants"), printAreas: z.record(z.string(), z.object({ position: z.string().describe("Print position (e.g., 'front', 'back')"), imageId: z.string().describe("Image ID from Printify uploads") })).optional().describe("Print areas for the product") }, async ({ title, description, blueprintId, printProviderId, variants, printAreas }) => { // Import the printify products service const { createProduct } = await import('./services/printify-products.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await createProduct(printifyClient, { title, description, blueprintId, printProviderId, variants, printAreas }); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Update product tool server.tool("update_product", { productId: z.string().describe("Product ID"), title: z.string().optional().describe("Product title"), description: z.string().optional().describe("Product description"), variants: z.array(z.object({ variantId: z.number().describe("Variant ID"), price: z.number().describe("Price in cents (e.g., 1999 for $19.99)"), isEnabled: z.boolean().optional().describe("Whether the variant is enabled") })).optional().describe("Product variants"), printAreas: z.record(z.string(), z.object({ position: z.string().describe("Print position (e.g., 'front', 'back')"), imageId: z.string().describe("Image ID from Printify uploads") })).optional().describe("Print areas for the product") }, async ({ productId, title, description, variants, printAreas }) => { // Import the printify products service const { updateProduct } = await import('./services/printify-products.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await updateProduct(printifyClient, productId, { title, description, variants, printAreas }); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Delete product tool server.tool("delete_product", { productId: z.string().describe("Product ID") }, async ({ productId }) => { // Import the printify products service const { deleteProduct } = await import('./services/printify-products.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await deleteProduct(printifyClient, productId); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Publish product tool server.tool("publish_product", { productId: z.string().describe("Product ID"), publishDetails: z.object({ title: z.boolean().optional().default(true).describe("Publish title"), description: z.boolean().optional().default(true).describe("Publish description"), images: z.boolean().optional().default(true).describe("Publish images"), variants: z.boolean().optional().default(true).describe("Publish variants"), tags: z.boolean().optional().default(true).describe("Publish tags") }).optional().describe("Publish details") }, async ({ productId, publishDetails }) => { // Import the printify products service const { publishProduct } = await import('./services/printify-products.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await publishProduct(printifyClient, productId, publishDetails); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Get blueprints tool server.tool("get_blueprints", { page: z.number().optional().default(1).describe("Page number"), limit: z.number().optional().default(10).describe("Number of blueprints per page") }, async ({ page, limit }) => { // Import the printify blueprints service const { getBlueprints } = await import('./services/printify-blueprints.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await getBlueprints(printifyClient, { page, limit }); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Get blueprint tool server.tool("get_blueprint", { blueprintId: z.string().describe("Blueprint ID") }, async ({ blueprintId }) => { // Import the printify blueprints service const { getBlueprint } = await import('./services/printify-blueprints.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await getBlueprint(printifyClient, blueprintId); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Get print providers tool server.tool("get_print_providers", { blueprintId: z.string().describe("Blueprint ID") }, async ({ blueprintId }) => { // Import the printify blueprints service const { getPrintProviders } = await import('./services/printify-blueprints.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await getPrintProviders(printifyClient, blueprintId); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Get variants tool server.tool("get_variants", { blueprintId: z.string().describe("Blueprint ID"), printProviderId: z.string().describe("Print provider ID") }, async ({ blueprintId, printProviderId }) => { // Import the printify blueprints service const { getVariants } = await import('./services/printify-blueprints.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Call the service const result = await getVariants(printifyClient, blueprintId, printProviderId); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Upload image tool server.tool("upload_image", { fileName: z.string().describe("File name"), url: z.string().describe("URL of the image to upload, path to local file, or base64 encoded image data") }, async ({ fileName, url }) => { // Import the printify uploader service const { uploadImageToPrintify, determineImageSourceType } = await import('./services/printify-uploader.js'); // Check if client is initialized if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Log the attempt with limited information for privacy const sourceType = determineImageSourceType(url); const sourcePreview = sourceType === 'url' ? url.substring(0, 30) + '...' : sourceType === 'file' ? url : // Show full file path url.substring(0, 30) + '...'; console.log(`Attempting to upload image: ${fileName} from ${sourceType} source: ${sourcePreview}`); // Call the service const result = await uploadImageToPrintify(printifyClient, fileName, url); // Return the result if (result.success) { return result.response; } else { return result.errorResponse; } }); // Imgur upload tool has been removed - we now upload directly to Printify // Get defaults tool server.tool("get_defaults", {}, async () => { try { if (!replicateClient) { return { content: [{ type: "text", text: "Replicate API client is not initialized. The REPLICATE_API_TOKEN environment variable may not be set." }], isError: true }; } const models = replicateClient.getAvailableModels(); const currentDefault = replicateClient.getDefault('model'); const allDefaults = replicateClient.getAllDefaults(); // Format the response in a user-friendly way const modelInfo = models.map(model => { if (model.id === currentDefault) { return `## ${model.name} ✓ SELECTED\n` + `- ID: \`${model.id}\`\n` + `- Description: ${model.description}\n` + `- Capabilities: ${model.capabilities.join(', ')}\n` + `- Status: **Currently selected as default model**\n`; } else { return `## ${model.name}\n` + `- ID: \`${model.id}\`\n` + `- Description: ${model.description}\n` + `- Capabilities: ${model.capabilities.join(', ')}\n`; } }).join('\n'); // Format all current defaults as a table const defaultsTable = Object.entries(allDefaults) .map(([key, val]) => `| ${key} | ${typeof val === 'object' ? JSON.stringify(val) : val} |`) .join('\n'); return { content: [{ type: "text", text: `# Current Default Settings\n\n` + `## Selected Model\n\n${modelInfo}\n\n` + `## All Default Parameters\n\n` + `| Option | Value |\n` + `|--------|-------|\n` + defaultsTable + `\n\n` + `To change any default setting, use the \`set_default\` tool:\n` + `\`\`\`javascript\n` + `set_default({ option: "model", value: "black-forest-labs/flux-1.1-pro-ultra" })\n` + `set_default({ option: "aspectRatio", value: "16:9" })\n` + `set_default({ option: "raw", value: false })\n` + `\`\`\`` }] }; } catch (error) { return { content: [{ type: "text", text: `Error getting defaults: ${error.message}` }], isError: true }; } }); // Note: The get-models tool has been removed in favor of the more general get_defaults tool // Set default parameter tool server.tool("set_default", { option: z.string().describe("The option name to set (e.g., 'model', 'aspectRatio', 'raw', etc.)"), value: z.any().describe("The value to set for the option") }, async ({ option, value }) => { try { if (!replicateClient) { return { content: [{ type: "text", text: "Replicate API client is not initialized. The REPLICATE_API_TOKEN environment variable may not be set." }], isError: true }; } // Set the default value replicateClient.setDefault(option, value); // Get all current defaults for the response const allDefaults = replicateClient.getAllDefaults(); // Format the response based on the option type let detailedResponse = ""; if (option === 'model') { // For model option, provide more detailed information const models = replicateClient.getAvailableModels(); const selectedModel = models.find(model => model.id === value); if (selectedModel) { detailedResponse = `## ${selectedModel.name} ✓ SELECTED\n` + `- ID: \`${selectedModel.id}\`\n` + `- Description: ${selectedModel.description}\n` + `- Capabilities: ${selectedModel.capabilities.join(', ')}\n` + `- Status: **Currently selected as default model**\n\n`; } } // Format all current defaults as a table const defaultsTable = Object.entries(allDefaults) .map(([key, val]) => `| ${key} | ${typeof val === 'object' ? JSON.stringify(val) : val} |`) .join('\n'); return { content: [{ type: "text", text: `# Default Setting Updated\n\n` + `Successfully set default \`${option}\` to: \`${value}\`\n\n` + detailedResponse + `## Current Default Settings\n\n` + `| Option | Value |\n` + `|--------|-------|\n` + defaultsTable + `\n\nThese settings will be used by default for all image generation unless overridden in the tool call.` }] }; } catch (error) { return { content: [{ type: "text", text: `Error setting default: ${error.message}` }], isError: true }; } }); // Note: The set-model tool has been removed in favor of the more general set_default tool // How to use Printify tool - provides detailed documentation on product creation workflow server.tool("how_to_use", { topic: z.enum([ "product_creation", "blueprints", "print_providers", "variants", "images", "publishing", "image_generation" ]).describe("The topic to get documentation for") }, async ({ topic }) => { try { // Import required modules const fs = await import('fs'); const path = await import('path'); const { promisify } = await import('util'); const readFile = promisify(fs.readFile); // Convert topic to file name format const fileName = `${topic}.md`; // Get the directory of the current file using import.meta.url // This works regardless of where the process is started from const { fileURLToPath } = await import('url'); const currentFilePath = fileURLToPath(import.meta.url); const currentDirPath = path.dirname(currentFilePath); const filePath = path.join(currentDirPath, 'docs', fileName); // Read the documentation file let documentation; try { documentation = await readFile(filePath, 'utf8'); } catch (readError) { // Get current working directory for debugging const cwd = process.cwd(); return { content: [{ type: "text", text: `Documentation for topic "${topic}" not found. Available topics are: product_creation, blueprints, print_providers, variants, images, publishing, image_generation\n\n` + `Debug info:\n` + `- Current working directory: ${cwd}\n` + `- Attempted file path: ${filePath}\n` + `- Error: ${readError.message}` }], isError: true }; } return { content: [{ type: "text", text: documentation }] }; } catch (error) { return { content: [{ type: "text", text: `Error retrieving documentation: ${error.message}` }], isError: true }; } }); // Add a prompt for product description generation server.prompt("generate_product_description", { productName: z.string(), category: z.string(), targetAudience: z.string().optional(), keyFeatures: z.string().optional().describe("Comma-separated list of key features") }, (args) => { const { productName, category, targetAudience, keyFeatures } = args; let featuresText = ""; if (keyFeatures) { const featuresList = keyFeatures.split(',').map(f => f.trim()); if (featuresList.length > 0) { featuresText = `\nKey features:\n${featuresList.map((f) => `- ${f}`).join('\n')}`; } } let audienceText = targetAudience ? `\nTarget audience: ${targetAudience}` : ""; return { messages: [{ role: "user", content: { type: "text", text: `Please write a compelling product description for the following product: Product name: ${productName} Category: ${category}${audienceText}${featuresText} The description should be engaging, highlight the benefits, and be suitable for an e-commerce platform.` } }] }; }); // Generate and upload image tool - combines Replicate image generation with Printify upload server.tool("generate_and_upload_image", { prompt: z.string().describe("Text prompt for image generation"), fileName: z.string().describe("File name for the uploaded image"), // Optional model override model: z.string().optional() .describe("Optional: Override the default model. Use get_defaults to see available models"), // Common parameters for both models width: z.number().optional().default(1024).describe("Image width in pixels"), height: z.number().optional().default(1024).describe("Image height in pixels"), aspectRatio: z.string().optional().describe("Aspect ratio (e.g., '16:9', '4:3', '1:1'). If provided, overrides width and height"), outputFormat: z.enum(["jpeg", "png", "webp"]).optional().default("png").describe("Output format"), safetyTolerance: z.number().optional().default(2).describe("Safety tolerance (0-6)"), seed: z.number().optional().describe("Random seed for reproducible generation"), numInferenceSteps: z.number().optional().default(25).describe("Number of inference steps"), guidanceScale: z.number().optional().default(7.5).describe("Guidance scale"), negativePrompt: z.string().optional().default("low quality, bad quality, sketches").describe("Negative prompt"), // Flux 1.1 Pro specific parameters promptUpsampling: z.boolean().optional() .describe("Enable prompt upsampling (Flux 1.1 Pro only)"), outputQuality: z.number().optional() .describe("Output quality 1-100 (Flux 1.1 Pro only)"), // Flux 1.1 Pro Ultra specific parameters raw: z.boolean().optional() .describe("Generate less processed, more natural-looking images (Flux 1.1 Pro Ultra only)"), imagePromptStrength: z.number().optional() .describe("Image prompt strength 0-1 (Flux 1.1 Pro Ultra only)") }, async ({ prompt, fileName, model, width, height, aspectRatio, outputFormat, safetyTolerance, seed, numInferenceSteps, guidanceScale, negativePrompt, promptUpsampling, outputQuality, raw, imagePromptStrength }) => { // Import the services const { generateImage } = await import('./services/image-generator.js'); const { formatSuccessResponse } = await import('./utils/error-handler.js'); // Check if clients are initialized if (!replicateClient) { return { content: [{ type: "text", text: "Replicate API client is not initialized. The REPLICATE_API_TOKEN environment variable may not be set." }], isError: true }; } if (!printifyClient) { return { content: [{ type: "text", text: "Printify API client is not initialized. The PRINTIFY_API_KEY environment variable may not be set." }], isError: true }; } // Check if we're using the Ultra model which requires ImgBB // Determine which model to use (user-specified or default) const modelToUse = model || replicateClient.getDefaultModel(); // Check if ImgBB API key is set when using Ultra model if (modelToUse.includes('flux-1.1-pro-ultra') && (!process.env.IMGBB_API_KEY || process.env.IMGBB_API_KEY === 'your-imgbb-api-key')) { return { content: [{ type: "text", text: `ERROR: The Flux 1.1 Pro Ultra model generates high-resolution images that are too large for direct base64 upload.\n\n` + `You MUST set the IMGBB_API_KEY environment variable when using this model.\n\n` + `Get a free API key from https://api.imgbb.com/ and add it to your .env file:\n` + `IMGBB_API_KEY=your_api_key_here` }], isError: true }; } // Check if a shop is selected const currentShop = printifyClient.getCurrentShop(); if (!currentShop) { return { content: [{ type: "text", text: "No shop is currently selected. Use the list_shops and switch_shop tools to select a shop." }], isError: true }; } console.log(`Starting generate_and_upload_image with prompt: ${prompt}`); // Get default parameters first const defaults = replicateClient.getAllDefaults(); // STEP 1: Generate the image with Replicate and process with Sharp // Start with defaults, then override with parameters from the tool call const generationResult = await generateImage(replicateClient, prompt, fileName, { // Start with defaults model: defaults.model, width: defaults.width, height: defaults.height, aspectRatio: defaults.aspectRatio, outputFormat: defaults.outputFormat, safetyTolerance: defaults.safetyTolerance, numInferenceSteps: defaults.numInferenceSteps, guidanceScale: defaults.guidanceScale, negativePrompt: defaults.negativePrompt, raw: defaults.raw, promptUpsampling: defaults.promptUpsampling, outputQuality: defaults.outputQuality, // Override with parameters from the tool call (if provided) ...(model !== undefined && { model }), ...(width !== undefined && { width }), ...(height !== undefined && { height }), ...(aspectRatio !== undefined && { aspectRatio }), ...(outputFormat !== undefined && { outputFormat }), ...(safetyTolerance !== undefined && { safetyTolerance }), ...(seed !== undefined && { seed }), ...(numInferenceSteps !== undefined && { numInferenceSteps }), ...(guidanceScale !== undefined && { guidanceScale }), ...(negativePrompt !== undefined && { negativePrompt }), ...(promptUpsampling !== undefined && { promptUpsampling }), ...(outputQuality !== undefined && { outputQuality }), ...(raw !== undefined && { raw }), ...(imagePromptStrength !== undefined && { imagePromptStrength }) }); // If image generation failed, return the error if (!generationResult.success) { return generationResult.errorResponse; } const imageBuffer = generationResult.buffer; const mimeType = generationResult.mimeType; const finalFileName = generationResult.fileName; const usingModel = generationResult.model; // Make sure we have valid image data if (!imageBuffer) { return { content: [{ type: "text", text: "Failed to get valid image data from the image generator." }], isError: true }; } // STEP 2: Upload the processed image to Printify console.log(`Uploading processed image to Printify`); console.log(`Image buffer size: ${imageBuffer.length} bytes`); console.log(`MIME type: ${mimeType}`); console.log(`File name: ${finalFileName}`); // Prepare for upload to Printify const uploadDetails = [ `Preparing to upload image to Printify:`, `- File name: ${finalFileName}`, `- Image buffer size: ${imageBuffer?.length || 0} bytes`, `- MIME type: ${mimeType}`, `- Model used: ${usingModel}` ].join('\n'); // Save the base64 data to a debug file for inspection try { const fs = await import('fs'); const path = await import('path'); // Create a debug directory if it doesn't exist const debugDir = path.join(process.cwd(), 'debug'); if (!fs.existsSync(debugDir)) { fs.mkdirSync(debugDir, { recursive: true }); } // Save the base64 data to a file for debugging const debugFilePath = path.join(debugDir, `debug_${Date.now()}_${finalFileName}`); // Save buffer directly to debug file if (imageBuffer) { fs.writeFileSync(debugFilePath, imageBuffer); console.log(`Saved image data to debug file: ${debugFilePath}`); console.log(`Debug file size: ${imageBuffer.length} bytes`); } else { console.error('No image data to save for debugging'); } } catch (debugError) { console.error('Error saving debug file:', debugError); } // Validate input data if (!imageBuffer) { return { content: [{ type: "text", text: "Error: No image data available for upload" }], isError: true }; } if (!finalFileName) { return { content: [{ type: "text", text: "Error: No filename available for upload" }], isError: true }; } // STEP 1: Import required modules let axios; let FormData; try { axios = (await import('axios')).default; FormData = (await import('form-data')).default; } catch (importError) { return { content: [{ type: "text", text: `Error importing required modules: ${importError.message || String(importError)}` }], isError: true }; } // STEP 2: Prepare image for upload (either via ImgBB or direct base64) let imageUrl; let uploadMethod = "direct"; // Check if we're using the Ultra model const isUsingUltraModel = usingModel.includes('flux-1.1-pro-ultra'); // Check if ImgBB API key is set const imgbbApiKey = process.env.IMGBB_API_KEY; if (imgbbApiKey && imgbbApiKey !== 'your-imgbb-api-key') { // If ImgBB API key is set, use ImgBB to get a URL try { // Create form data for ImgBB const formData = new FormData(); // Convert buffer to base64 for ImgBB upload const base64Data = imageBuffer.toString('base64'); formData.append('image', base64Data); // Upload to ImgBB with the key as a query parameter const imgbbResponse = await axios.post(`https://api.imgbb.com/1/upload?key=${imgbbApiKey}`, formData); // Get the image URL from ImgBB response imageUrl = imgbbResponse.data.data.url; uploadMethod = "imgbb"; // Log success console.log(`Successfully uploaded image to ImgBB. URL: ${imageUrl}`); } catch (imgbbError) { // Only fall back to direct upload if not using Ultra model if (isUsingUltraModel) { return { content: [{ type: "text", text: `Error uploading to ImgBB: ${imgbbError.message || String(imgbbError)}\n\n` + `When using the Ultra model, ImgBB upload is required and cannot be bypassed.\n\n` + `Response data: ${JSON.stringify(imgbbError.response?.data || {}, null, 2)}` }], isError: true }; } console.log(`Error uploading to ImgBB: ${imgbbError.message || String(imgbbError)}. Falling back to direct base64 upload.`); // Fall back to direct base64 upload for non-Ultra models uploadMethod = "direct"; } } else if (!isUsingUltraModel) { console.log("No ImgBB API key found. Using direct base64 upload."); } // STEP 4: Import Printify SDK let Printify; try { Printify = (await import('printify-sdk-js')).default; } catch (importError) { return { content: [{ type: "text", text: `Error importing Printify SDK: ${importError.message || String(importError)}\n\n` + `ImgBB URL: ${imageUrl}` }], isError: true }; } // STEP 5: Create Printify client let printifySDK; try { printifySDK = new Printify({ accessToken: process.env.PRINTIFY_API_KEY || '', shopId: printifyClient ? printifyClient.getCurrentShopId() || undefined : undefined }); // Log client creation console.log(`Created Printify client with shop ID: ${printifyClient?.getCurrentShopId() || 'undefined'}`); } catch (clientError) { return { content: [{ type: "text", text: `Error creating Printify client: ${clientError.message || String(clientError)}\n\n` + `ImgBB URL: ${imageUrl}` }], isError: true }; } // STEP 6: Upload the image to Printify let image; try { if (uploadMethod === "imgbb" && imageUrl) { // Upload using the URL from ImgBB image = await printifySDK.uploads.uploadImage({ file_name: finalFileName, url: imageUrl }); console.log(`Successfully uploaded image to Printify using ImgBB URL. Image ID: ${image.id}`); } else { // Direct base64 upload // Convert buffer to base64 for Printify direct upload const base64Data = imageBuffer.toString('base64'); image = await printifySDK.uploads.uploadImage({ file_name: finalFileName, contents: base64Data }); console.log(`Successfully uploaded image to Printify using direct base64. Image ID: ${image.id}`); } } catch (uploadError) { return { content: [{ type: "text", text: `Error uploading to Printify: ${uploadError.message || String(uploadError)}\n\n` + `Upload method: ${uploadMethod}${imageUrl ? `\nImgBB URL: ${imageUrl}` : ''}\n\n` + `Response data: ${JSON.stringify(uploadError.response?.data || {}, null, 2)}` }], isError: true }; } // STEP 7: Return success response const response = formatSuccessResponse('Image Generated and Uploaded Successfully', { Prompt: prompt, Model: usingModel.split('/')[1], 'Image ID': image.id, 'File Name': image.file_name, Dimensions: `${image.width}x${image.height}`, 'Preview URL': image.preview_url, 'Upload Method': uploadMethod === "imgbb" ? "ImgBB URL" : "Direct base64", ...(imageUrl ? { 'ImgBB URL': imageUrl } : {}), 'Upload Details': uploadDetails }, `You can now use this image ID (${image.id}) when creating a product.\n\n` + `**Example:**\n` + `\`\`\`json\n` + `"print_areas": {\n` + ` "front": { "position": "front", "imageId": "${image.id}" }\n` + `}\n` + `\`\`\``); return response; }); // Generate image tool - uses Replicate directly without Printify integration server.tool("generate_image", { prompt: z.string().describe("Text prompt for image generation"), outputPath: z.string().describe("Full path where the generated image should be saved"), // Optional model override model: z.string().optional() .describe("Optional: Override the default model. Use get_defaults to see available models"), // Common parameters for both models width: z.number().optional().default(1024).describe("Image width in pixels"), height: z.number().optional().default(1024).describe("Image height in pixels"), aspectRatio: z.string().optional().describe("Aspect ratio (e.g., '16:9', '4:3', '1:1'). If provided, overrides width and height"), outputFormat: z.enum(["jpeg", "png", "webp"]).optional().default("png").describe("Output format"), safetyTolerance: z.number().optional().default(2).describe("Safety tolerance (0-6)"), seed: z.number().optional().describe("Random seed for reproducible generation"), numInferenceSteps: z.number().optional().default(25).describe("Number of inference steps"), guidanceScale: z.number().optional().default(7.5).describe("Guidance scale"), negativePrompt: z.string().optional().default("low quality, bad quality, sketches").describe("Negative prompt"), // Flux 1.1 Pro specific parameters promptUpsampling: z.boolean().optional() .describe("Enable prompt upsampling (Flux 1.1 Pro only)"), outputQuality: z.number().optional() .describe("Output quality 1-100 (Flux 1.1 Pro only)"), // Flux 1.1 Pro Ultra specific parameters raw: z.boolean().optional() .describe("Generate less processed, more natural-looking images (Flux 1.1 Pro Ultra only)"), imagePromptStrength: z.number().optional() .describe("Image prompt strength 0-1 (Flux 1.1 Pro Ultra only)") }, async ({ prompt, outputPath, model, width, height, aspectRatio, outputFormat, safetyTolerance, seed, numInferenceSteps, guidanceScale, negativePrompt, promptUpsampling, outputQuality, raw, imagePromptStrength }) => { // Import the services const { generateImage } = await import('./services/image-generator.js'); const { formatSuccessResponse } = await import('./utils/error-handler.js'); const fs = await import('fs'); const path = await import('path'); // Check if Replicate client is initialized if (!replicateClient) { return { content: [{ type: "text", text: "Replicate API client is not initialized. The REPLICATE_API_TOKEN environment variable may not be set." }], isError: true }; } // Extract filename from the output path const fileName = path.basename(outputPath); // Check if we're using the Ultra model which requires ImgBB // Determine which model to use (user-specified or default) const modelToUse = model || replicateClient.getDefaultModel(); console.log(`Starting generate_image with prompt: ${prompt}`); console.log(`Using model: ${modelToUse}`); console.log(`Output path: ${outputPath}`); // Get default parameters first const defaults = replicateClient.getAllDefaults(); // Generate the image with Replicate and process with Sharp // Start with defaults, then override with parameters from the tool call const generationResult = await generateImage(replicateClient, prompt, fileName, { // Start with defaults model: defaults.model, width: defaults.width, height: defaults.height, aspectRatio: defaults.aspectRatio, outputFormat: defaults.outputFormat, safetyTolerance: defaults.safetyTolerance, numInferenceSteps: defaults.numInferenceSteps, guidanceScale: defaults.guidanceScale, negativePrompt: defaults.negativePrompt, raw: defaults.raw, promptUpsampling: defaults.promptUpsampling, outputQuality: defaults.outputQuality, // Override with parameters from the tool call (if provided) ...(model !== undefined && { model }), ...(width !== undefined && { width }), ...(height !== undefined && { height }), ...(aspectRatio !== undefined && { aspectRatio }), ...(outputFormat !== undefined && { outputFormat }), ...(safetyTolerance !== undefined && { safetyTolerance }), ...(seed !== undefined && { seed }), ...(numInferenceSteps !== undefined && { numInferenceSteps }), ...(guidanceScale !== undefined && { guidanceScale }), ...(negativePrompt !== undefined && { negativePrompt }), ...(promptUpsampling !== undefined && { promptUpsampling }), ...(outputQuality !== undefined && { outputQuality }), ...(raw !== undefined && { raw }), ...(imagePromptStrength !== undefined && { imagePromptStrength }) }); // If image generation failed, return the error if (!generationResult.success) { return generationResult.errorResponse; } const imageBuffer = generationResult.buffer; const finalFileName = generationResult.fileName; const usingModel = generationResult.model; const dimensions = generationResult.dimensions; // Make sure we have valid image data if (!imageBuffer) { return { content: [{ type: "text", text: "Failed to get valid image data from the image generator." }], isError: true }; } try { // Create the directory if it doesn't exist const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Save the buffer directly to the specified output path if (imageBuffer) { fs.writeFileSync(outputPath, imageBuffer); } else { throw new Error('No image data available to save'); } // Return success response const response = formatSuccessResponse('Image Generated Successfully', { Prompt: prompt, Model: usingModel.split('/')[1], 'Output Path': outputPath, 'File Name': finalFileName, 'File Size': `${imageBuffer ? imageBuffer.length : 0} bytes`, 'Dimensions': dimensions || `${width}x${height}`, 'Format': outputFormat || 'png', 'Generation Parameters': { // Use the actual dimensions from the generated image ...(generationResult.dimensions ? { 'Dimensions': generationResult.dimensions } : {}), // Show the aspect ratio that was actually used (from tool call or defaults) 'Aspect Ratio': aspectRatio || defaults.aspectRatio || '1:1', 'Inference Steps': numInferenceSteps || defaults.numInferenceSteps, 'Guidance Scale': guidanceScale || defaults.guidanceScale, 'Negative Prompt': negativePrompt || defaults.negativePrompt, ...(raw !== undefined ? { 'Raw Mode': raw } : {}), ...(promptUpsampling !== undefined ? { 'Prompt Upsampling': promptUpsampling } : {}), ...(outputQuality !== undefined ? { 'Output Quality': outputQuality } : {}), ...(imagePromptStrength !== undefined ? { 'Image Prompt Strength': imagePromptStrength } : {}), ...(seed !== undefined ? { 'Seed': seed } : {})