Skip to main content
Glama

export_marketing

Exports a flat, opaque 1024x1024 PNG for App Store Connect marketing assets. Removes Liquid Glass effects and alpha channel for direct upload.

Instructions

Export a flat marketing PNG for App Store Connect. No Liquid Glass effects, no alpha channel. Produces a 1024x1024 (default) opaque PNG ready for upload.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
bundle_pathYesPath to .icon bundle
output_pathYesOutput path for the PNG file
sizeNoOutput size in pixels (default 1024)
return_imageNoReturn the rendered image inline as base64 (default true)

Implementation Reference

  • Main handler function for export_marketing. Reads the icon bundle, renders a flat preview, strips alpha channel using background color from manifest fill, writes to output_path, and optionally returns inline base64 image.
    export async function exportMarketing(params: ExportMarketingParams): Promise<McpResult> {
      try {
        const size = params.size ?? 1024;
        const { manifest, assets } = await readIconBundle(params.bundle_path);
    
        // Render flat preview (no ictool, no glass)
        let buffer = await renderPreview(manifest, assets, size);
    
        // Determine background color from manifest fill for alpha flattening
        const fill = resolveFill(manifest);
        let bgColor = { r: 255, g: 255, b: 255 };
        if (fill && typeof fill === 'object' && 'solid' in fill) {
          const parts = fill.solid.split(':')[1]?.split(',').map(Number);
          if (parts && parts.length >= 3) {
            bgColor = {
              r: Math.round(parts[0] * 255),
              g: Math.round(parts[1] * 255),
              b: Math.round(parts[2] * 255),
            };
          }
        }
    
        buffer = await stripAlpha(buffer, bgColor);
        await fs.writeFile(params.output_path, buffer);
    
        const stat = await fs.stat(params.output_path);
        const content: McpContentBlock[] = [
          { type: 'text', text: `Exported marketing icon to ${params.output_path} (${size}x${size}, no alpha, ${(stat.size / 1024).toFixed(1)} KB)` },
        ];
        if (params.return_image !== false && buffer.length <= MAX_INLINE_IMAGE_BYTES) {
          content.push({ type: 'image', data: buffer.toString('base64'), mimeType: 'image/png' });
        }
        return { content };
      } catch (error: unknown) {
        const msg = error instanceof Error ? error.message : 'Unknown error';
        return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
      }
    }
  • Input parameters interface for export_marketing: bundle_path, output_path, size (default 1024), return_image (default true).
    export interface ExportMarketingParams {
      bundle_path: string;
      output_path: string;
      size?: number;
      return_image?: boolean;
    }
  • src/server.ts:259-270 (registration)
    MCP tool registration for export_marketing with Zod schema and description. Registered via server.tool() on the MCP server.
    // ── Tool: export_marketing ──
    server.tool(
      'export_marketing',
      'Export a flat marketing PNG for App Store Connect. No Liquid Glass effects, no alpha channel. Produces a 1024x1024 (default) opaque PNG ready for upload.',
      {
        bundle_path: z.string().describe('Path to .icon bundle'),
        output_path: z.string().describe('Output path for the PNG file'),
        size: z.number().min(16).max(2048).default(1024).describe('Output size in pixels (default 1024)'),
        return_image: z.boolean().default(true).describe('Return the rendered image inline as base64 (default true)'),
      },
      async (params) => exportMarketing(params),
    );
  • src/cli.ts:372-388 (registration)
    CLI command registration for 'export-marketing' as a commander.js command.
    // ── export-marketing ──
    
    program
      .command('export-marketing')
      .description('Export flat marketing PNG for App Store Connect (no alpha)')
      .argument('<bundle_path>', 'Path to the .icon bundle')
      .argument('<output_path>', 'Output PNG path')
      .option('--size <n>', 'Output size in pixels', toInt, 1024)
      .action(async (bundle_path, output_path, opts) => {
        await run(() =>
          exportMarketing({
            bundle_path,
            output_path,
            size: opts.size,
          }),
        );
      });
  • Imports used by exportMarketing: readIconBundle (to load .icon bundle), renderPreview (to render flat image), resolveFill (to get background color from manifest), stripAlpha (to remove alpha channel).
    import * as fs from 'node:fs/promises';
    import sharp from 'sharp';
    import { readIconBundle, saveManifest } from './bundle';
    import { renderPreview, compositeOnBackground, type CanvasBackground, type ApplePresetName } from './render';
    import { resolveFill } from './manifest';
    import { ictoolAvailable, renderWithIctool, CLEAR_RENDITIONS } from './ictool';
    import { stripAlpha } from './image-utils';
    import type { IconManifest, McpResult, McpContentBlock } from '../types';
    
    const MAX_INLINE_IMAGE_BYTES = 4 * 1024 * 1024; // 4 MB
    
    // ictool and Icon Composer use the manifest scale values directly.
    // scale=1.0 renders at ~65% of icon area — this is Apple's native behavior.
    // Our flat renderer applies the same 0.65 factor to match.
    
    export interface ExportPreviewParams {
      bundle_path: string;
      output_path: string;
      size: number;
      appearance?: 'dark' | 'tinted';
      flat: boolean;
      canvas_bg?: string;
      apple_preset?: string;
      canvas_bg_color?: string;
      canvas_bg_image?: string;
      zoom: number;
      return_image?: boolean;
    }
    
    export interface RenderLiquidGlassParams {
      bundle_path: string;
      output_path: string;
      platform: string;
      rendition: string;
      width: number;
      height: number;
      scale: number;
      light_angle?: number;
      tint_color?: number;
      tint_strength?: number;
      canvas_bg?: string;
      apple_preset?: string;
      canvas_bg_color?: string;
      canvas_bg_image?: string;
      zoom: number;
      return_image?: boolean;
    }
    
    export function resolveCanvasBackgroundParam(params: {
      canvas_bg_image?: string;
      canvas_bg_color?: string;
      apple_preset?: string;
      canvas_bg?: string;
    }): CanvasBackground {
      if (params.canvas_bg_image) {
        return { type: 'image', path: params.canvas_bg_image };
      } else if (params.canvas_bg_color) {
        return { type: 'solid', color: params.canvas_bg_color };
      } else if (params.apple_preset) {
        return { type: 'apple-preset', name: params.apple_preset as ApplePresetName };
      } else if (params.canvas_bg && params.canvas_bg !== 'none') {
        return { type: 'preset', name: params.canvas_bg as any };
      }
      return { type: 'none' };
    }
    
    export async function exportPreview(params: ExportPreviewParams): Promise<McpResult> {
      try {
        const useIctool = !params.flat && await ictoolAvailable();
    
        const renditionMap: Record<string, string> = { dark: 'Dark', tinted: 'TintedLight' };
        const rendition = params.appearance ? renditionMap[params.appearance] ?? 'Default' : 'Default';
    
        let buffer: Buffer;
        let renderer: string;
    
        if (useIctool) {
          const canvasBg = resolveCanvasBackgroundParam(params);
          const hasCanvas = canvasBg.type !== 'none' || params.zoom !== 1.0;
    
          const tmpPath = params.output_path + '.ictool.png';
          try {
            await renderWithIctool({
              bundlePath: params.bundle_path,
              outputPath: tmpPath,
              rendition,
              width: params.size,
              height: params.size,
            });
            const raw = await fs.readFile(tmpPath);
    
            if (hasCanvas) {
              // Canvas will be composited later — keep full squircle
              buffer = raw;
            } else {
              // No canvas — glass glyph without squircle outline.
              // 1. Scale down glyph in manifest (smaller relative to app outline)
              // 2. Render ictool at bigger size (outline pushed outside crop zone)
              // 3. Crop center at target size — outline gone, glyph at correct px
              // The two factors cancel: glyph pixels = same as normal render.
              const INSCRIBED_RATIO = 0.55;
              const renderSize = Math.ceil(params.size / INSCRIBED_RATIO);
    
              // Temporarily shrink layer scales in the manifest
              const { manifest } = await readIconBundle(params.bundle_path);
              const origScales: number[] = [];
              for (const group of manifest.groups) {
                for (const layer of group.layers) {
                  const pos = layer.position ?? { scale: 1.0, 'translation-in-points': [0, 0] as [number, number] };
                  if (!layer.position) layer.position = pos;
                  origScales.push(pos.scale);
                  pos.scale *= INSCRIBED_RATIO;
                }
              }
              await saveManifest(params.bundle_path, manifest);
    
              const tmpLarge = params.output_path + '.ictool-large.png';
              try {
                await renderWithIctool({
                  bundlePath: params.bundle_path,
                  outputPath: tmpLarge,
                  rendition,
                  width: renderSize,
                  height: renderSize,
                });
                const largeRaw = await fs.readFile(tmpLarge);
    
                // Crop center at target size — squircle outline is outside
                const cropOffset = Math.round((renderSize - params.size) / 2);
                const cropped = await sharp(largeRaw)
                  .extract({ left: cropOffset, top: cropOffset, width: params.size, height: params.size })
                  .png()
                  .toBuffer();
    
                // Composite onto fill-color canvas
                const fill = resolveFill(manifest, params.appearance);
                let bgColor = { r: 255, g: 255, b: 255 };
                if (fill && typeof fill === 'object' && 'solid' in fill) {
                  const parts = fill.solid.split(':')[1]?.split(',').map(Number);
                  if (parts && parts.length >= 3) {
                    bgColor = {
                      r: Math.round(parts[0] * 255),
                      g: Math.round(parts[1] * 255),
                      b: Math.round(parts[2] * 255),
                    };
                  }
                }
                buffer = await sharp({
                  create: { width: params.size, height: params.size, channels: 4, background: { ...bgColor, alpha: 255 } },
                })
                  .composite([{ input: cropped, left: 0, top: 0 }])
                  .png()
                  .toBuffer();
              } finally {
                // Restore original scales
                let i = 0;
                for (const group of manifest.groups) {
                  for (const layer of group.layers) {
                    if (layer.position && i < origScales.length) {
                      layer.position.scale = origScales[i++];
                    }
                  }
                }
                await saveManifest(params.bundle_path, manifest);
                await fs.unlink(tmpLarge).catch(() => {});
              }
            }
          } finally {
            await fs.unlink(tmpPath).catch(() => {});
          }
          renderer = 'liquid-glass';
        } else {
          const { manifest, assets } = await readIconBundle(params.bundle_path);
          buffer = await renderPreview(manifest, assets, params.size, params.appearance);
          renderer = 'flat';
        }
    
        const canvasBg = resolveCanvasBackgroundParam(params);
    
        if (canvasBg.type !== 'none' || params.zoom !== 1.0) {
          const iconSize = Math.round(params.size * params.zoom);
          buffer = await compositeOnBackground(buffer, canvasBg, params.size, iconSize);
        }
    
        await fs.writeFile(params.output_path, buffer);
    
        const content: McpContentBlock[] = [
          { type: 'text', text: `Exported preview to ${params.output_path} (${params.size}x${params.size}, ${renderer}, zoom: ${params.zoom}x, bg: ${params.canvas_bg_image ? 'image' : params.canvas_bg_color ?? params.canvas_bg ?? 'none'})` },
        ];
        if (params.return_image !== false && buffer.length <= MAX_INLINE_IMAGE_BYTES) {
          content.push({ type: 'image', data: buffer.toString('base64'), mimeType: 'image/png' });
        }
        return { content };
      } catch (error: unknown) {
        const msg = error instanceof Error ? error.message : 'Unknown error';
        return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
      }
    }
    
    export async function renderLiquidGlass(params: RenderLiquidGlassParams): Promise<McpResult> {
      try {
        if (!await ictoolAvailable()) {
          return {
            content: [{ type: 'text', text: 'Error: Icon Composer.app not found at /Applications/Icon Composer.app. Install it from developer.apple.com/icon-composer/' }],
            isError: true,
          };
        }
    
        await renderWithIctool({
          bundlePath: params.bundle_path,
          outputPath: params.output_path,
          platform: params.platform,
          rendition: params.rendition,
          width: params.width,
          height: params.height,
          scale: params.scale,
          lightAngle: params.light_angle,
          tintColor: params.tint_color,
          tintStrength: params.tint_strength,
        });
    
        const hasBackground = params.canvas_bg_image || params.canvas_bg_color || params.apple_preset || (params.canvas_bg && params.canvas_bg !== 'none');
        if (CLEAR_RENDITIONS.has(params.rendition) && hasBackground) {
          return {
            content: [{ type: 'text', text: `ClearLight/ClearDark renditions do not support canvas backgrounds. Apple's glass transparency effect requires a Metal GPU pipeline that isn't available via CLI. Use Default, Dark, or Tinted renditions for background compositing.` }],
            isError: true,
          };
        }
    
        const canvasBg = resolveCanvasBackgroundParam(params);
    
        if (canvasBg.type !== 'none' || params.zoom !== 1.0) {
          const iconBuffer = await fs.readFile(params.output_path);
          const canvasSize = Math.max(params.width, params.height);
          const iconSize = Math.round(canvasSize * params.zoom);
          const result = await compositeOnBackground(iconBuffer, canvasBg, canvasSize, iconSize);
          await fs.writeFile(params.output_path, result);
        }
    
        const stat = await fs.stat(params.output_path);
    
        const content: McpContentBlock[] = [
          { type: 'text', text: `Rendered Liquid Glass preview to ${params.output_path} (${params.width}x${params.height}@${params.scale}x, ${params.rendition}, zoom: ${params.zoom}x, ${(stat.size / 1024).toFixed(1)} KB)` },
        ];
        if (params.return_image !== false) {
          const fileBuffer = await fs.readFile(params.output_path);
          if (fileBuffer.length <= MAX_INLINE_IMAGE_BYTES) {
            content.push({ type: 'image', data: fileBuffer.toString('base64'), mimeType: 'image/png' });
          }
        }
        return { content };
      } catch (error: unknown) {
        const msg = error instanceof Error ? error.message : 'Unknown error';
        return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
      }
    }
Behavior3/5

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

No annotations are provided, so the description carries full burden. It discloses key behaviors (no effects, no alpha, opaque, default size) but omits details about the return_image parameter behavior and output_path expectations. The description is adequate but not fully transparent about all parameter-driven behaviors.

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

Conciseness5/5

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

Two sentences, no redundancy, front-loaded with the core action. Every word adds value. Excellent conciseness.

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

Completeness3/5

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

Given 4 parameters, no output schema, and no annotations, the description covers the main purpose and constraints but does not explain the return_image parameter or output_path format. It is sufficient for a straightforward export tool but could be more complete.

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

Parameters3/5

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

Schema description coverage is 100%, so baseline is 3. The description adds minimal extra parameter meaning beyond the schema (mentions default 1024x1024 size, which is already in schema). It does not elaborate on return_image or output_path beyond what the schema provides.

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

Purpose5/5

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

The description clearly states it exports a flat marketing PNG for App Store Connect, specifying no Liquid Glass effects, no alpha channel, and default 1024x1024 size. It distinguishes from siblings like render_liquid_glass and export_preview by focusing on the flat, opaque export use case.

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

Usage Guidelines4/5

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

The description explicitly says 'No Liquid Glass effects, no alpha channel', which tells the agent not to use this tool when effects are needed. It implies the tool is for final marketing export, but lacks explicit when-to-use vs alternatives like export_preview. Still provides good contextual guidance.

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

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/ethbak/icon-composer-mcp'

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