Skip to main content
Glama

upload_image

Upload images to the Printify MCP Server by providing a file name and source (URL, local path, or base64 data) for integration with AI-driven print-on-demand workflows.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
fileNameYesFile name
urlYesURL of the image to upload, path to local file, or base64 encoded image data

Implementation Reference

  • src/index.ts:558-599 (registration)
    MCP server.tool registration for 'upload_image', including input schema (fileName, url) and handler function that checks client, determines source type, and delegates to uploadImageToPrintify service
    // 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 }): Promise<{ content: any[], isError?: boolean }> => {
        // 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 as { content: any[], isError?: boolean };
        } else {
          return result.errorResponse as { content: any[], isError: boolean };
        }
      }
    );
  • Main handler function uploadImageToPrintify that validates shop, handles file/url/base64 sources, verifies files, and calls PrintifyAPI.uploadImage
    export async function uploadImageToPrintify(
      printifyClient: PrintifyAPI,
      fileName: string,
      source: string
    ) {
      try {
        // Validate shop is selected
        const currentShop = printifyClient.getCurrentShop();
        if (!currentShop) {
          throw new Error('No shop is currently selected. Use the list-shops and switch-shop tools to select a shop.');
        }
    
        // Determine the source type
        const sourceType = determineImageSourceType(source);
        console.log(`Uploading image to Printify from ${sourceType} source`);
    
        let image;
    
        if (sourceType === 'file') {
          // Handle file upload
          const filePath = normalizeFilePath(source);
    
          // Validate file exists
          const fileInfo = getFileInfo(filePath);
          if (!fileInfo.exists) {
            throw new Error(`File not found: ${filePath}`);
          }
    
          console.log(`Uploading file to Printify: ${filePath}`);
          console.log(`File size: ${fileInfo.size} bytes`);
    
          // Check file size limits
          if (fileInfo.size && fileInfo.size > 20 * 1024 * 1024) { // 20MB limit
            throw new Error(`File is too large (${Math.round(fileInfo.size / (1024 * 1024))}MB). Maximum size is 20MB.`);
          }
    
          // Verify the file exists and is readable before uploading
          try {
            // Use dynamic imports for fs and path
            const fs = await import('fs');
            const path = await import('path');
    
            if (fs.existsSync(filePath)) {
              const stats = fs.statSync(filePath);
              console.log(`File verification before upload:`);
              console.log(`- Path: ${filePath}`);
              console.log(`- Absolute path: ${path.resolve(filePath)}`);
              console.log(`- Size: ${stats.size} bytes`);
              console.log(`- Created: ${stats.birthtime}`);
              console.log(`- Permissions: ${stats.mode.toString(8)}`);
    
              try {
                fs.accessSync(filePath, fs.constants.R_OK);
                console.log(`- Readable: Yes`);
              } catch (e: any) {
                console.log(`- Readable: No - ${e.message || e}`);
              }
    
              // Try to read the first few bytes to verify the file is readable
              try {
                const fd = fs.openSync(filePath, 'r');
                const buffer = Buffer.alloc(10);
                const bytesRead = fs.readSync(fd, buffer, 0, 10, 0);
                fs.closeSync(fd);
                console.log(`- Read test: Successfully read ${bytesRead} bytes`);
              } catch (readError: any) {
                console.error(`- Read test failed: ${readError.message || readError}`);
              }
    
              // Create a debug directory and copy the file there
              try {
                const debugDir = path.join(process.cwd(), 'debug');
                if (!fs.existsSync(debugDir)) {
                  fs.mkdirSync(debugDir, { recursive: true });
                }
    
                const debugFilePath = path.join(debugDir, `upload_${Date.now()}_${path.basename(filePath)}`);
                fs.copyFileSync(filePath, debugFilePath);
                console.log(`- Debug copy: ${debugFilePath}`);
              } catch (copyError: any) {
                console.error(`- Debug copy failed: ${copyError.message || copyError}`);
              }
            } else {
              console.error(`ERROR: File does not exist at upload time: ${filePath}`);
              throw new Error(`File does not exist at upload time: ${filePath}`);
            }
          } catch (verifyError: any) {
            console.error('Error verifying file before upload:', verifyError);
            throw new Error(`Failed to verify file before upload: ${verifyError.message || verifyError}`);
          }
    
          // Upload to Printify
          console.log(`Attempting to upload file to Printify: ${filePath}`);
          image = await printifyClient.uploadImage(fileName, filePath);
          console.log(`Upload successful! Image ID: ${image.id}`);
          console.log(`Preview URL: ${image.preview_url}`);
        } else {
          // For URLs and base64 strings, upload directly
          image = await printifyClient.uploadImage(fileName, source);
        }
    
        console.log(`Image uploaded successfully! ID: ${image.id}`);
    
        return {
          success: true,
          image,
          response: formatSuccessResponse(
            'Image Uploaded Successfully',
            {
              'Image ID': image.id,
              'File Name': image.file_name,
              'Dimensions': `${image.width}x${image.height}`,
              'Preview URL': image.preview_url
            },
            `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` +
            `\`\`\``
          )
        };
      } catch (error: any) {
        console.error('Error uploading image to Printify:', error);
    
        // Determine source type for better error messages
        const sourceType = determineImageSourceType(source);
        const sourceTypeLabel = sourceType === 'url' ? 'URL' :
                               sourceType === 'file' ? 'file path' :
                               'base64 string';
    
        // Create appropriate troubleshooting tips based on source type
        const tips = [
          'Check that your Printify API key is valid',
          'Ensure your Printify account is properly connected'
        ];
    
        if (sourceType === 'url') {
          tips.push('Make sure the URL is publicly accessible and points directly to an image file');
          tips.push('The URL must start with http:// or https://');
        } else if (sourceType === 'file') {
          tips.push('Make sure the file exists and is readable');
          tips.push('Check that the path is correct and includes the full path to the file');
          tips.push('The file must be a valid image format (PNG, JPEG, SVG)');
          tips.push('Recommended resolution for JPEG/PNG files is 300 DPI');
          tips.push('Maximum file size is 20MB');
        } else {
          tips.push('Make sure the base64 string is valid and represents an image');
        }
    
        // Gather as much diagnostic information as possible
        const diagnosticInfo: any = {
          FileName: fileName,
          SourceType: sourceTypeLabel,
          Source: sourceType === 'url' ? source : (sourceType === 'file' ? source : `${source.substring(0, 30)}...`),
          CurrentShop: printifyClient.getCurrentShop(),
          ErrorType: error.constructor.name,
          ErrorStack: error.stack,
          ErrorMessage: error.message,
          CurrentWorkingDirectory: process.cwd(),
          NodeVersion: process.version,
          Platform: process.platform,
          // Add Printify client information
          PrintifyClientInitialized: !!printifyClient,
          PrintifyShopId: printifyClient.getCurrentShopId(),
          PrintifyAvailableShops: printifyClient.getAvailableShops().length
        };
    
        // Add file-specific diagnostics if it's a file
        if (sourceType === 'file') {
          const filePath = normalizeFilePath(source);
          const fileInfo = getFileInfo(filePath);
          diagnosticInfo.FileExists = fileInfo.exists;
          diagnosticInfo.FileSize = fileInfo.exists ? fileInfo.size + ' bytes' : 'N/A';
    
          // Try to get more file details if it exists
          if (fileInfo.exists) {
            try {
              // Use dynamic imports for fs
              const fsPromise = import('fs');
              const pathPromise = import('path');
    
              // Wait for imports to complete
              const [fsModule, pathModule] = await Promise.all([fsPromise, pathPromise]);
              const fs = fsModule.default || fsModule;
              const path = pathModule.default || pathModule;
    
              const stats = fs.statSync(filePath);
              diagnosticInfo.FileCreated = stats.birthtime;
              diagnosticInfo.FileModified = stats.mtime;
              diagnosticInfo.FilePermissions = stats.mode.toString(8);
              diagnosticInfo.AbsolutePath = path.resolve(filePath);
    
              // Try to read the first few bytes to verify content
              try {
                const buffer = Buffer.alloc(10);
                const fd = fs.openSync(filePath, 'r');
                // Read the first 10 bytes
                const bytesRead = fs.readSync(fd, buffer, 0, 10, 0);
                fs.closeSync(fd);
                diagnosticInfo.FileReadable = true;
                diagnosticInfo.BytesRead = bytesRead;
                diagnosticInfo.FileFirstBytes = buffer.toString('hex').substring(0, 20);
    
                // Check file signature to determine if it's a valid image
                const hexSignature = buffer.toString('hex').substring(0, 8).toLowerCase();
                let fileType = 'unknown';
    
                // Check common image signatures
                if (hexSignature.startsWith('89504e47')) {
                  fileType = 'PNG';
                } else if (hexSignature.startsWith('ffd8ffe')) {
                  fileType = 'JPEG';
                } else if (hexSignature.startsWith('52494646')) {
                  fileType = 'WEBP';
                } else if (hexSignature.startsWith('3c737667')) {
                  fileType = 'SVG';
                }
    
                diagnosticInfo.DetectedFileType = fileType;
                diagnosticInfo.FileSignature = hexSignature;
              } catch (readError: any) {
                diagnosticInfo.FileReadable = false;
                diagnosticInfo.FileReadError = readError.message || String(readError);
              }
            } catch (statError: any) {
              diagnosticInfo.FileStatError = statError.message || String(statError);
            }
          }
        }
    
        // Add error details if available
        if (error.response) {
          diagnosticInfo.PrintifyResponseStatus = error.response.status;
          diagnosticInfo.PrintifyResponseStatusText = error.response.statusText;
          diagnosticInfo.PrintifyResponseData = error.response.data;
          diagnosticInfo.PrintifyResponseHeaders = error.response.headers;
        }
    
        return {
          success: false,
          error,
          errorResponse: formatErrorResponse(
            error,
            `Printify Upload (${sourceTypeLabel})`,
            diagnosticInfo,
            tips
          )
        };
      }
    }
  • Zod input schema for upload_image tool: fileName (string), url (string supporting URL, local file path, or base64)
    {
      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")
    },
  • PrintifyAPI.uploadImage low-level helper: handles URL direct upload, file reading+Sharp+base64 conversion, data URL or plain base64 upload to Printify SDK
    async uploadImage(fileName: string, source: string) {
      try {
        console.log(`Uploading image ${fileName}`);
    
        // If the source starts with http:// or https://, use the URL upload method
        if (source.startsWith('http://') || source.startsWith('https://')) {
          console.log(`Uploading from URL: ${source.substring(0, 30)}...`);
          return await this.client.uploads.uploadImage({ file_name: fileName, url: source });
        }
    
        // If it's a file path, try to read the file and convert to base64
        if (source.startsWith('file://') || source.includes(':\\') || source.includes(':/') || !source.startsWith('data:')) {
          try {
            console.log(`Attempting to read file from: ${source}`);
    
            // Handle file:// protocol
            let filePath = source;
            if (source.startsWith('file:///')) {
              filePath = source.replace('file:///', '');
            }
    
            // Handle Windows paths
            if (filePath.startsWith('/')) {
              filePath = filePath.substring(1);
            }
    
            console.log(`Normalized file path: ${filePath}`);
    
            // Check if file exists
            if (!fs.existsSync(filePath)) {
              const error = new Error(`File not found: ${filePath}`);
              console.error('File not found error:', error);
              console.error('Current working directory:', process.cwd());
              console.error('File path type:', typeof filePath);
              console.error('Absolute path check:', path.isAbsolute(filePath) ? 'Absolute' : 'Relative');
    
              // Try to list the directory contents if possible
              try {
                const dir = path.dirname(filePath);
                if (fs.existsSync(dir)) {
                  console.error('Directory exists. Contents:', fs.readdirSync(dir));
                } else {
                  console.error('Parent directory does not exist:', dir);
                }
              } catch (dirError) {
                console.error('Error checking directory:', dirError);
              }
    
              throw error;
            }
    
            // Get file stats
            const stats = fs.statSync(filePath);
            console.log(`File size: ${stats.size} bytes`);
    
            if (stats.size === 0) {
              throw new Error(`File is empty: ${filePath}`);
            }
    
            if (stats.size > 10 * 1024 * 1024) { // 10MB limit
              throw new Error(`File is too large (${Math.round(stats.size / (1024 * 1024))}MB). Maximum size is 10MB.`);
            }
    
            // Process the image with Sharp
            console.log('Processing image with Sharp before uploading...');
    
            // Use Sharp directly
            const sharpInstance = sharp(filePath);
            const outputFormat = path.extname(filePath).toLowerCase() === '.jpg' ||
                                path.extname(filePath).toLowerCase() === '.jpeg' ? 'jpeg' : 'png';
    
            // Convert to the appropriate format
            if (outputFormat === 'jpeg') {
              sharpInstance.jpeg({ quality: 100 });
            } else {
              sharpInstance.png({ quality: 100 });
            }
    
            // Get the buffer
            const buffer = await sharpInstance.toBuffer();
            console.log(`Image processed successfully: ${buffer.length} bytes`);
    
            // Determine the MIME type
            const mimeType = outputFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
    
            // Convert to base64
            const base64Data = buffer.toString('base64');
            console.log(`Converted to base64 string of length ${base64Data.length}`);
    
            // Create data URL with the proper MIME type prefix
            const dataUrl = `data:${mimeType};base64,${base64Data}`;
            console.log(`Uploading with data URL (MIME type: ${mimeType})`);
    
            try {
              console.log(`Uploading to Printify with file_name: ${fileName}, contents length: ${base64Data.length}`);
              // Use the dataUrl instead of just the base64Data
              const result = await this.client.uploads.uploadImage({ file_name: fileName, contents: dataUrl.split(',')[1] });
              console.log('Upload successful, result:', result);
              return result;
            } catch (uploadError: any) {
              console.error('Error during Printify upload:', uploadError);
              if (uploadError.response) {
                console.error('Response status:', uploadError.response.status);
                console.error('Response data:', JSON.stringify(uploadError.response.data, null, 2));
              }
              throw uploadError;
            }
          } catch (error: any) {
            console.error('Error reading file:', error);
            const errorMessage = error.message || 'Unknown error';
    
            // Create a detailed error message with troubleshooting information
            let detailedError = `Failed to process file ${source}: ${errorMessage}\n\n`;
            detailedError += 'Troubleshooting steps:\n';
            detailedError += '1. Check if the file exists and is readable\n';
            detailedError += '2. Make sure the file is a valid image (PNG, JPEG, etc.)\n';
            detailedError += '3. Try using a URL or base64 encoded string instead\n';
            detailedError += '\nFile processing details:\n';
            detailedError += `- Attempted to read from: ${source}\n`;
            detailedError += `- Current working directory: ${process.cwd()}\n`;
    
            // Add stack trace
            if (error.stack) {
              detailedError += `\nStack trace:\n${error.stack}\n`;
            }
    
            throw new Error(detailedError);
          }
        } else if (source.startsWith('data:image/')) {
          // If source is base64 data with data URL prefix
          // Extract the base64 content
          const base64Content = source.split(',')[1];
          console.log(`Uploading image with base64 data from data URL (length: ${base64Content.length})`);
          return await this.client.uploads.uploadImage({ file_name: fileName, contents: base64Content });
        } else {
          // Otherwise, assume it's a base64 encoded string without prefix
          console.log(`Uploading image with base64 data (length: ${source.length})`);
          return await this.client.uploads.uploadImage({ file_name: fileName, contents: source });
        }
      } catch (error: any) {
        console.error('Error uploading image:', error);
    
        // Add detailed debugging information
        const debugInfo: any = {
          fileName,
          sourceType: typeof source,
          sourceLength: source.length,
          currentWorkingDir: process.cwd(),
          errorMessage: error.message,
          errorStack: error.stack
        };
    
        console.error('Detailed upload error information:', JSON.stringify(debugInfo, null, 2));
    
        // If there's a response object, extract and log the full response data
        if (error.response) {
          console.error('Response status:', error.response.status);
          console.error('Response data:', JSON.stringify(error.response.data, null, 2));
    
          // Add the full response data to the debug info
          debugInfo.responseStatus = error.response.status;
          debugInfo.responseData = error.response.data;
        }
    
        throw this.enhanceError(error, debugInfo);
      }
    }
  • determineImageSourceType utility to classify image source as 'url', 'file', or 'base64' for routing logic
    export function determineImageSourceType(source: string): 'url' | 'file' | 'base64' {
      // Check if it's a URL
      if (source.startsWith('http://') || source.startsWith('https://')) {
        return 'url';
      }
    
      // Check if it's a file path
      if (source.includes(':\\') || source.includes(':/') || source.startsWith('/') || source.includes('\\')) {
        return 'file';
      }
    
      // Otherwise assume it's base64
      return 'base64';
    }
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Tool has no description.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness1/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Tool has no description.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness1/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Tool has no description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Tool has no description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose1/5

Does the description clearly state what the tool does and how it differs from similar tools?

Tool has no description.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines1/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Tool has no description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TSavo/printify-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server