@takeshijuan/ideogram-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/dist/index.js +3607 -0
  4. package/package.json +77 -0
package/dist/index.js ADDED
@@ -0,0 +1,3607 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/config/constants.ts
13
+ var API_BASE_URL, API_ENDPOINTS, API_KEY_HEADER, DEFAULTS, VALIDATION, CREDITS_PER_IMAGE, EDIT_CREDITS_PER_IMAGE, USD_PER_CREDIT, HTTP_STATUS, RETRYABLE_STATUS_CODES, RETRY_CONFIG, TIMEOUTS, SERVER_INFO, PREDICTION_QUEUE, ERROR_CODES;
14
+ var init_constants = __esm({
15
+ "src/config/constants.ts"() {
16
+ "use strict";
17
+ API_BASE_URL = "https://api.ideogram.ai";
18
+ API_ENDPOINTS = {
19
+ /** V3 Generate endpoint */
20
+ GENERATE_V3: "/v1/ideogram-v3/generate",
21
+ /** Legacy Edit endpoint (inpainting only) */
22
+ EDIT_LEGACY: "/edit",
23
+ /** Legacy V2 Generate endpoint */
24
+ GENERATE_LEGACY: "/generate"
25
+ };
26
+ API_KEY_HEADER = "Api-Key";
27
+ DEFAULTS = {
28
+ /** Default aspect ratio */
29
+ ASPECT_RATIO: "1x1",
30
+ /** Default number of images to generate */
31
+ NUM_IMAGES: 1,
32
+ /** Default rendering speed */
33
+ RENDERING_SPEED: "DEFAULT",
34
+ /** Default magic prompt setting */
35
+ MAGIC_PROMPT: "AUTO",
36
+ /** Default style type */
37
+ STYLE_TYPE: "AUTO",
38
+ /** Default save locally option */
39
+ SAVE_LOCALLY: true
40
+ };
41
+ VALIDATION = {
42
+ /** Prompt constraints */
43
+ PROMPT: {
44
+ MIN_LENGTH: 1,
45
+ MAX_LENGTH: 1e4
46
+ },
47
+ /** Number of images constraints */
48
+ NUM_IMAGES: {
49
+ MIN: 1,
50
+ MAX: 8
51
+ },
52
+ /** Seed constraints */
53
+ SEED: {
54
+ MIN: 0,
55
+ MAX: 2147483647
56
+ },
57
+ /** Expand pixels constraints for outpainting */
58
+ EXPAND_PIXELS: {
59
+ MIN: 1,
60
+ MAX: 1024
61
+ },
62
+ /** Image file constraints */
63
+ IMAGE: {
64
+ /** Maximum file size in bytes (10MB) */
65
+ MAX_SIZE_BYTES: 10 * 1024 * 1024,
66
+ /** Supported image formats */
67
+ SUPPORTED_FORMATS: ["image/png", "image/jpeg", "image/webp"]
68
+ }
69
+ };
70
+ CREDITS_PER_IMAGE = {
71
+ FLASH: 0.04,
72
+ TURBO: 0.08,
73
+ DEFAULT: 0.1,
74
+ QUALITY: 0.2
75
+ };
76
+ EDIT_CREDITS_PER_IMAGE = {
77
+ FLASH: 0.06,
78
+ TURBO: 0.1,
79
+ DEFAULT: 0.12,
80
+ QUALITY: 0.24
81
+ };
82
+ USD_PER_CREDIT = 0.05;
83
+ HTTP_STATUS = {
84
+ OK: 200,
85
+ CREATED: 201,
86
+ BAD_REQUEST: 400,
87
+ UNAUTHORIZED: 401,
88
+ FORBIDDEN: 403,
89
+ NOT_FOUND: 404,
90
+ TOO_MANY_REQUESTS: 429,
91
+ INTERNAL_SERVER_ERROR: 500,
92
+ SERVICE_UNAVAILABLE: 503
93
+ };
94
+ RETRYABLE_STATUS_CODES = [
95
+ HTTP_STATUS.TOO_MANY_REQUESTS,
96
+ HTTP_STATUS.INTERNAL_SERVER_ERROR,
97
+ HTTP_STATUS.SERVICE_UNAVAILABLE
98
+ ];
99
+ RETRY_CONFIG = {
100
+ /** Maximum number of retry attempts */
101
+ MAX_ATTEMPTS: 3,
102
+ /** Initial delay in milliseconds */
103
+ INITIAL_DELAY_MS: 1e3,
104
+ /** Maximum delay in milliseconds */
105
+ MAX_DELAY_MS: 1e4,
106
+ /** Backoff multiplier for exponential backoff */
107
+ BACKOFF_MULTIPLIER: 2
108
+ };
109
+ TIMEOUTS = {
110
+ /** Default API request timeout */
111
+ DEFAULT_REQUEST_MS: 3e4,
112
+ /** Long-running request timeout (for quality rendering) */
113
+ LONG_REQUEST_MS: 12e4,
114
+ /** Image download timeout */
115
+ IMAGE_DOWNLOAD_MS: 6e4
116
+ };
117
+ SERVER_INFO = {
118
+ NAME: "ideogram-mcp-server",
119
+ VERSION: "1.0.0",
120
+ DESCRIPTION: "Production-grade Ideogram AI image generation MCP server"
121
+ };
122
+ PREDICTION_QUEUE = {
123
+ /** Maximum number of queued predictions */
124
+ MAX_QUEUE_SIZE: 100,
125
+ /** Prediction timeout in milliseconds */
126
+ PREDICTION_TIMEOUT_MS: 3e5,
127
+ /** Time before prediction records are cleaned up (24 hours) */
128
+ CLEANUP_AGE_MS: 24 * 60 * 60 * 1e3
129
+ };
130
+ ERROR_CODES = {
131
+ // Authentication errors
132
+ INVALID_API_KEY: "INVALID_API_KEY",
133
+ MISSING_API_KEY: "MISSING_API_KEY",
134
+ // Validation errors
135
+ VALIDATION_ERROR: "VALIDATION_ERROR",
136
+ INVALID_PROMPT: "INVALID_PROMPT",
137
+ INVALID_ASPECT_RATIO: "INVALID_ASPECT_RATIO",
138
+ INVALID_IMAGE: "INVALID_IMAGE",
139
+ INVALID_MASK: "INVALID_MASK",
140
+ IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
141
+ // API errors
142
+ API_ERROR: "API_ERROR",
143
+ RATE_LIMITED: "RATE_LIMITED",
144
+ INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
145
+ // Network errors
146
+ NETWORK_ERROR: "NETWORK_ERROR",
147
+ TIMEOUT: "TIMEOUT",
148
+ // Prediction errors
149
+ PREDICTION_NOT_FOUND: "PREDICTION_NOT_FOUND",
150
+ PREDICTION_ALREADY_COMPLETED: "PREDICTION_ALREADY_COMPLETED",
151
+ PREDICTION_FAILED: "PREDICTION_FAILED",
152
+ // Storage errors
153
+ STORAGE_ERROR: "STORAGE_ERROR",
154
+ DOWNLOAD_FAILED: "DOWNLOAD_FAILED",
155
+ // General errors
156
+ INTERNAL_ERROR: "INTERNAL_ERROR",
157
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
158
+ };
159
+ }
160
+ });
161
+
162
+ // src/utils/error.handler.ts
163
+ function createInvalidApiKeyError(details) {
164
+ return new IdeogramMCPError(
165
+ ERROR_CODES.INVALID_API_KEY,
166
+ "Invalid or missing Ideogram API key",
167
+ "Your API key is invalid or has been revoked. Please check your IDEOGRAM_API_KEY environment variable and ensure it is a valid API key from https://ideogram.ai/manage-api",
168
+ HTTP_STATUS.UNAUTHORIZED,
169
+ false,
170
+ details
171
+ );
172
+ }
173
+ function createMissingApiKeyError() {
174
+ return new IdeogramMCPError(
175
+ ERROR_CODES.MISSING_API_KEY,
176
+ "Ideogram API key not configured",
177
+ "No API key found. Please set the IDEOGRAM_API_KEY environment variable with your API key from https://ideogram.ai/manage-api",
178
+ HTTP_STATUS.UNAUTHORIZED,
179
+ false
180
+ );
181
+ }
182
+ function createRateLimitError(retryAfterSeconds) {
183
+ const retryMessage = retryAfterSeconds ? ` Please wait ${retryAfterSeconds} seconds before retrying.` : " Please wait a moment and try again.";
184
+ return new IdeogramMCPError(
185
+ ERROR_CODES.RATE_LIMITED,
186
+ `Rate limit exceeded${retryAfterSeconds ? ` (retry after ${retryAfterSeconds}s)` : ""}`,
187
+ `Too many requests.${retryMessage}`,
188
+ HTTP_STATUS.TOO_MANY_REQUESTS,
189
+ true,
190
+ retryAfterSeconds ? { retry_after_seconds: retryAfterSeconds } : void 0
191
+ );
192
+ }
193
+ function createInsufficientCreditsError(requiredCredits, availableCredits) {
194
+ const details = {};
195
+ if (requiredCredits !== void 0) {
196
+ details["required_credits"] = requiredCredits;
197
+ }
198
+ if (availableCredits !== void 0) {
199
+ details["available_credits"] = availableCredits;
200
+ }
201
+ return new IdeogramMCPError(
202
+ ERROR_CODES.INSUFFICIENT_CREDITS,
203
+ "Insufficient credits to complete the request",
204
+ "You do not have enough credits to complete this request. Please purchase more credits at https://ideogram.ai",
205
+ HTTP_STATUS.FORBIDDEN,
206
+ false,
207
+ Object.keys(details).length > 0 ? details : void 0
208
+ );
209
+ }
210
+ function createInvalidImageError(reason) {
211
+ return new IdeogramMCPError(
212
+ ERROR_CODES.INVALID_IMAGE,
213
+ `Invalid image: ${reason}`,
214
+ `There was a problem with the provided image: ${reason}`,
215
+ HTTP_STATUS.BAD_REQUEST,
216
+ false,
217
+ { field: "image" }
218
+ );
219
+ }
220
+ function createImageTooLargeError(sizeBytes, maxSizeBytes) {
221
+ const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(2);
222
+ const maxSizeMB = (maxSizeBytes / (1024 * 1024)).toFixed(0);
223
+ return new IdeogramMCPError(
224
+ ERROR_CODES.IMAGE_TOO_LARGE,
225
+ `Image size ${sizeMB}MB exceeds maximum ${maxSizeMB}MB`,
226
+ `The image is too large (${sizeMB}MB). Maximum allowed size is ${maxSizeMB}MB.`,
227
+ HTTP_STATUS.BAD_REQUEST,
228
+ false,
229
+ { size_bytes: sizeBytes, max_size_bytes: maxSizeBytes }
230
+ );
231
+ }
232
+ function createNetworkError(reason, originalError) {
233
+ return new IdeogramMCPError(
234
+ ERROR_CODES.NETWORK_ERROR,
235
+ `Network error: ${reason}`,
236
+ "A network error occurred. Please check your internet connection and try again.",
237
+ 0,
238
+ true,
239
+ originalError ? { original_error: originalError.message } : void 0
240
+ );
241
+ }
242
+ function createTimeoutError(timeoutMs) {
243
+ return new IdeogramMCPError(
244
+ ERROR_CODES.TIMEOUT,
245
+ `Request timed out after ${timeoutMs}ms`,
246
+ "The request took too long to complete. Please try again.",
247
+ 0,
248
+ true,
249
+ { timeout_ms: timeoutMs }
250
+ );
251
+ }
252
+ function createPredictionNotFoundError(predictionId) {
253
+ return new IdeogramMCPError(
254
+ ERROR_CODES.PREDICTION_NOT_FOUND,
255
+ `Prediction not found: ${predictionId}`,
256
+ "The requested prediction was not found. It may have expired or never existed.",
257
+ HTTP_STATUS.NOT_FOUND,
258
+ false,
259
+ { prediction_id: predictionId }
260
+ );
261
+ }
262
+ function createStorageError(operation, reason) {
263
+ return new IdeogramMCPError(
264
+ ERROR_CODES.STORAGE_ERROR,
265
+ `Storage error during ${operation}: ${reason}`,
266
+ `Failed to save image: ${reason}`,
267
+ 0,
268
+ true,
269
+ { operation }
270
+ );
271
+ }
272
+ function createDownloadFailedError(url, reason) {
273
+ return new IdeogramMCPError(
274
+ ERROR_CODES.DOWNLOAD_FAILED,
275
+ `Failed to download image from ${url}: ${reason}`,
276
+ "Failed to download the generated image. The URL may have expired.",
277
+ 0,
278
+ true,
279
+ { url }
280
+ );
281
+ }
282
+ function createInternalError(reason, details) {
283
+ return new IdeogramMCPError(
284
+ ERROR_CODES.INTERNAL_ERROR,
285
+ `Internal error: ${reason}`,
286
+ "An unexpected error occurred. Please try again.",
287
+ HTTP_STATUS.INTERNAL_SERVER_ERROR,
288
+ true,
289
+ details
290
+ );
291
+ }
292
+ function createApiError(statusCode, message, details) {
293
+ const retryable = RETRYABLE_STATUS_CODES.includes(statusCode);
294
+ return new IdeogramMCPError(
295
+ ERROR_CODES.API_ERROR,
296
+ `API error (${statusCode}): ${message}`,
297
+ getApiErrorUserMessage(statusCode, message),
298
+ statusCode,
299
+ retryable,
300
+ details
301
+ );
302
+ }
303
+ function fromApiErrorResponse(statusCode, response) {
304
+ if (statusCode === HTTP_STATUS.UNAUTHORIZED) {
305
+ return createInvalidApiKeyError(response.details);
306
+ }
307
+ if (statusCode === HTTP_STATUS.TOO_MANY_REQUESTS) {
308
+ return createRateLimitError();
309
+ }
310
+ if (statusCode === HTTP_STATUS.FORBIDDEN) {
311
+ if (response.message.toLowerCase().includes("credit") || response.message.toLowerCase().includes("balance")) {
312
+ return createInsufficientCreditsError();
313
+ }
314
+ }
315
+ return createApiError(statusCode, response.message, response.details);
316
+ }
317
+ function fromAxiosError(error) {
318
+ if (!error.response) {
319
+ if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
320
+ return createTimeoutError(3e4);
321
+ }
322
+ return createNetworkError(error.message);
323
+ }
324
+ const { status, data } = error.response;
325
+ if (data && typeof data === "object" && "message" in data) {
326
+ return fromApiErrorResponse(status, data);
327
+ }
328
+ const message = typeof data === "string" ? data : error.message;
329
+ return createApiError(status, message);
330
+ }
331
+ function wrapError(error) {
332
+ if (error instanceof IdeogramMCPError) {
333
+ return error;
334
+ }
335
+ if (error instanceof Error) {
336
+ return createInternalError(error.message, { original_error: error.name });
337
+ }
338
+ if (typeof error === "string") {
339
+ return createInternalError(error);
340
+ }
341
+ return createInternalError("An unknown error occurred");
342
+ }
343
+ function isIdeogramMCPError(error) {
344
+ return error instanceof IdeogramMCPError;
345
+ }
346
+ function isRetryableError(error) {
347
+ if (isIdeogramMCPError(error)) {
348
+ return error.retryable;
349
+ }
350
+ return false;
351
+ }
352
+ function getApiErrorUserMessage(statusCode, apiMessage) {
353
+ switch (statusCode) {
354
+ case HTTP_STATUS.BAD_REQUEST:
355
+ return `Invalid request: ${apiMessage}`;
356
+ case HTTP_STATUS.UNAUTHORIZED:
357
+ return "Authentication failed. Please check your API key.";
358
+ case HTTP_STATUS.FORBIDDEN:
359
+ return "Access denied. You may not have permission for this operation.";
360
+ case HTTP_STATUS.NOT_FOUND:
361
+ return "The requested resource was not found.";
362
+ case HTTP_STATUS.TOO_MANY_REQUESTS:
363
+ return "Too many requests. Please wait before trying again.";
364
+ case HTTP_STATUS.INTERNAL_SERVER_ERROR:
365
+ return "The Ideogram API encountered an error. Please try again.";
366
+ case HTTP_STATUS.SERVICE_UNAVAILABLE:
367
+ return "The Ideogram API is temporarily unavailable. Please try again later.";
368
+ default:
369
+ return `An error occurred: ${apiMessage}`;
370
+ }
371
+ }
372
+ function extractRetryAfter(headers) {
373
+ if (!headers)
374
+ return void 0;
375
+ const retryAfter = headers["retry-after"] || headers["Retry-After"];
376
+ if (!retryAfter)
377
+ return void 0;
378
+ const seconds = parseInt(retryAfter, 10);
379
+ if (!isNaN(seconds))
380
+ return seconds;
381
+ const date = new Date(retryAfter);
382
+ if (!isNaN(date.getTime())) {
383
+ const nowMs = Date.now();
384
+ const retryMs = date.getTime();
385
+ return Math.max(0, Math.ceil((retryMs - nowMs) / 1e3));
386
+ }
387
+ return void 0;
388
+ }
389
+ var IdeogramMCPError;
390
+ var init_error_handler = __esm({
391
+ "src/utils/error.handler.ts"() {
392
+ "use strict";
393
+ init_constants();
394
+ IdeogramMCPError = class _IdeogramMCPError extends Error {
395
+ /**
396
+ * Creates a new IdeogramMCPError instance.
397
+ *
398
+ * @param code - Error code for programmatic handling
399
+ * @param message - Technical error message for debugging
400
+ * @param userMessage - User-friendly message suitable for display
401
+ * @param statusCode - HTTP status code (or 0 for non-HTTP errors)
402
+ * @param retryable - Whether the operation can be safely retried
403
+ * @param details - Additional error context for debugging
404
+ */
405
+ constructor(code, message, userMessage, statusCode, retryable, details) {
406
+ super(message);
407
+ this.code = code;
408
+ this.userMessage = userMessage;
409
+ this.statusCode = statusCode;
410
+ this.retryable = retryable;
411
+ this.details = details;
412
+ Object.setPrototypeOf(this, _IdeogramMCPError.prototype);
413
+ if (Error.captureStackTrace) {
414
+ Error.captureStackTrace(this, _IdeogramMCPError);
415
+ }
416
+ }
417
+ /**
418
+ * The error name for stack traces
419
+ */
420
+ name = "IdeogramMCPError";
421
+ /**
422
+ * Converts the error to a ToolErrorOutput format for MCP tool responses.
423
+ */
424
+ toToolError() {
425
+ const base = {
426
+ success: false,
427
+ error_code: this.code,
428
+ error: this.message,
429
+ user_message: this.userMessage,
430
+ retryable: this.retryable
431
+ };
432
+ if (this.details !== void 0) {
433
+ return { ...base, details: this.details };
434
+ }
435
+ return base;
436
+ }
437
+ /**
438
+ * Creates a JSON-serializable representation of the error.
439
+ */
440
+ toJSON() {
441
+ return {
442
+ name: this.name,
443
+ code: this.code,
444
+ message: this.message,
445
+ userMessage: this.userMessage,
446
+ statusCode: this.statusCode,
447
+ retryable: this.retryable,
448
+ details: this.details,
449
+ stack: this.stack
450
+ };
451
+ }
452
+ };
453
+ }
454
+ });
455
+
456
+ // src/config/config.ts
457
+ import { config as dotenvConfig } from "dotenv";
458
+ import { z } from "zod";
459
+ function parseBoolean(value, defaultValue) {
460
+ if (value === void 0 || value === "") {
461
+ return defaultValue;
462
+ }
463
+ const normalized = value.toLowerCase().trim();
464
+ if (["true", "1", "yes", "on"].includes(normalized)) {
465
+ return true;
466
+ }
467
+ if (["false", "0", "no", "off"].includes(normalized)) {
468
+ return false;
469
+ }
470
+ return defaultValue;
471
+ }
472
+ function parseInteger(value, defaultValue) {
473
+ if (value === void 0 || value === "") {
474
+ return defaultValue;
475
+ }
476
+ const parsed = parseInt(value, 10);
477
+ if (Number.isNaN(parsed)) {
478
+ return defaultValue;
479
+ }
480
+ return parsed;
481
+ }
482
+ function loadConfig() {
483
+ const result = ConfigSchema.safeParse(rawConfig);
484
+ if (!result.success) {
485
+ const errorMessages = result.error.issues.map((issue) => {
486
+ const path2 = issue.path.join(".");
487
+ return ` - ${path2}: ${issue.message}`;
488
+ }).join("\n");
489
+ throw new Error(
490
+ `Configuration validation failed:
491
+ ${errorMessages}
492
+
493
+ Please check your environment variables or .env file.`
494
+ );
495
+ }
496
+ return result.data;
497
+ }
498
+ function isConfigValid() {
499
+ return ConfigSchema.safeParse(rawConfig).success;
500
+ }
501
+ function getConfigErrors() {
502
+ const result = ConfigSchema.safeParse(rawConfig);
503
+ if (result.success) {
504
+ return [];
505
+ }
506
+ return result.error.issues.map((issue) => {
507
+ const path2 = issue.path.join(".");
508
+ return `${path2}: ${issue.message}`;
509
+ });
510
+ }
511
+ var LOG_LEVELS, ConfigSchema, rawConfig, config;
512
+ var init_config = __esm({
513
+ "src/config/config.ts"() {
514
+ "use strict";
515
+ init_constants();
516
+ dotenvConfig();
517
+ LOG_LEVELS = ["debug", "info", "warn", "error"];
518
+ ConfigSchema = z.object({
519
+ /**
520
+ * Ideogram API key (required)
521
+ */
522
+ ideogramApiKey: z.string({
523
+ required_error: "IDEOGRAM_API_KEY is required. Get your API key from https://ideogram.ai/manage-api"
524
+ }).min(1, {
525
+ message: "IDEOGRAM_API_KEY cannot be empty. Get your API key from https://ideogram.ai/manage-api"
526
+ }),
527
+ /**
528
+ * Logging level
529
+ */
530
+ logLevel: z.enum(LOG_LEVELS).default("info"),
531
+ /**
532
+ * Directory for local image storage
533
+ */
534
+ localSaveDir: z.string().default("./ideogram_images"),
535
+ /**
536
+ * Enable automatic local saving of images
537
+ */
538
+ enableLocalSave: z.boolean().default(true),
539
+ /**
540
+ * Maximum concurrent API requests
541
+ */
542
+ maxConcurrentRequests: z.number().int().min(1, { message: "MAX_CONCURRENT_REQUESTS must be at least 1" }).max(10, {
543
+ message: "MAX_CONCURRENT_REQUESTS cannot exceed 10 to prevent rate limiting"
544
+ }).default(3),
545
+ /**
546
+ * API request timeout in milliseconds
547
+ */
548
+ requestTimeoutMs: z.number().int().min(1e3, { message: "REQUEST_TIMEOUT_MS must be at least 1000ms" }).max(3e5, { message: "REQUEST_TIMEOUT_MS cannot exceed 300000ms (5 minutes)" }).default(TIMEOUTS.DEFAULT_REQUEST_MS)
549
+ });
550
+ rawConfig = {
551
+ ideogramApiKey: process.env["IDEOGRAM_API_KEY"],
552
+ logLevel: process.env["LOG_LEVEL"],
553
+ localSaveDir: process.env["LOCAL_SAVE_DIR"],
554
+ enableLocalSave: parseBoolean(process.env["ENABLE_LOCAL_SAVE"], DEFAULTS.SAVE_LOCALLY),
555
+ maxConcurrentRequests: parseInteger(process.env["MAX_CONCURRENT_REQUESTS"], 3),
556
+ requestTimeoutMs: parseInteger(
557
+ process.env["REQUEST_TIMEOUT_MS"],
558
+ TIMEOUTS.DEFAULT_REQUEST_MS
559
+ )
560
+ };
561
+ config = loadConfig();
562
+ }
563
+ });
564
+
565
+ // src/utils/logger.ts
566
+ import { pino } from "pino";
567
+ function isDevelopment() {
568
+ const nodeEnv = process.env["NODE_ENV"];
569
+ return nodeEnv === "development" || nodeEnv === void 0;
570
+ }
571
+ function shouldPrettyPrint() {
572
+ const prettyPrint = process.env["LOG_PRETTY"];
573
+ if (prettyPrint === "true" || prettyPrint === "1") {
574
+ return true;
575
+ }
576
+ if (prettyPrint === "false" || prettyPrint === "0") {
577
+ return false;
578
+ }
579
+ return isDevelopment();
580
+ }
581
+ function createLoggerOptions() {
582
+ const level = LOG_LEVEL_MAP[config.logLevel];
583
+ const baseOptions = {
584
+ name: SERVER_INFO.NAME,
585
+ level,
586
+ // Base context included in all log entries
587
+ base: {
588
+ service: SERVER_INFO.NAME,
589
+ version: SERVER_INFO.VERSION
590
+ },
591
+ // Custom timestamp format
592
+ timestamp: pino.stdTimeFunctions.isoTime,
593
+ // Customize error serialization
594
+ serializers: {
595
+ err: pino.stdSerializers.err,
596
+ error: pino.stdSerializers.err,
597
+ req: serializeRequest,
598
+ res: serializeResponse
599
+ },
600
+ // Format options for structured output
601
+ formatters: {
602
+ level: (label) => ({ level: label }),
603
+ bindings: (bindings) => ({
604
+ pid: bindings["pid"],
605
+ hostname: bindings["hostname"],
606
+ service: bindings["service"],
607
+ version: bindings["version"]
608
+ })
609
+ }
610
+ };
611
+ if (shouldPrettyPrint()) {
612
+ return {
613
+ ...baseOptions,
614
+ transport: {
615
+ target: "pino-pretty",
616
+ options: {
617
+ colorize: true,
618
+ translateTime: "SYS:standard",
619
+ ignore: "pid,hostname"
620
+ }
621
+ }
622
+ };
623
+ }
624
+ return baseOptions;
625
+ }
626
+ function serializeRequest(req) {
627
+ if (!req || typeof req !== "object") {
628
+ return {};
629
+ }
630
+ const serialized = {};
631
+ if ("method" in req)
632
+ serialized["method"] = req["method"];
633
+ if ("url" in req)
634
+ serialized["url"] = req["url"];
635
+ if ("endpoint" in req)
636
+ serialized["endpoint"] = req["endpoint"];
637
+ if ("headers" in req && typeof req["headers"] === "object" && req["headers"] !== null) {
638
+ const headers = req["headers"];
639
+ const sanitizedHeaders = {};
640
+ for (const [key, value] of Object.entries(headers)) {
641
+ const lowerKey = key.toLowerCase();
642
+ if (lowerKey === "authorization" || lowerKey === "api-key" || lowerKey === "x-api-key" || lowerKey === "cookie") {
643
+ sanitizedHeaders[key] = "[REDACTED]";
644
+ } else {
645
+ sanitizedHeaders[key] = value;
646
+ }
647
+ }
648
+ serialized["headers"] = sanitizedHeaders;
649
+ }
650
+ return serialized;
651
+ }
652
+ function serializeResponse(res) {
653
+ if (!res || typeof res !== "object") {
654
+ return {};
655
+ }
656
+ const serialized = {};
657
+ if ("status" in res)
658
+ serialized["status"] = res["status"];
659
+ if ("statusCode" in res)
660
+ serialized["statusCode"] = res["statusCode"];
661
+ if ("duration" in res)
662
+ serialized["duration"] = res["duration"];
663
+ return serialized;
664
+ }
665
+ function createChildLogger(component) {
666
+ return logger.child({ component });
667
+ }
668
+ function logApiRequest(log, context) {
669
+ log.debug(
670
+ {
671
+ req: {
672
+ method: context.method,
673
+ endpoint: context.endpoint
674
+ },
675
+ hasImage: context.hasImage,
676
+ hasMask: context.hasMask
677
+ },
678
+ "Ideogram API request"
679
+ );
680
+ }
681
+ function logApiResponse(log, context) {
682
+ const level = context.statusCode >= 400 ? "warn" : "debug";
683
+ log[level](
684
+ {
685
+ res: {
686
+ statusCode: context.statusCode,
687
+ duration: context.durationMs
688
+ },
689
+ endpoint: context.endpoint,
690
+ imageCount: context.imageCount
691
+ },
692
+ "Ideogram API response"
693
+ );
694
+ }
695
+ function logToolInvocation(log, context) {
696
+ const sanitizedParams = sanitizeToolParams(context.params);
697
+ log.info(
698
+ { tool: context.tool, params: sanitizedParams },
699
+ "Tool invoked"
700
+ );
701
+ }
702
+ function logToolResult(log, context) {
703
+ const level = context.success ? "info" : "warn";
704
+ log[level](
705
+ {
706
+ tool: context.tool,
707
+ success: context.success,
708
+ durationMs: context.durationMs,
709
+ errorCode: context.errorCode
710
+ },
711
+ context.success ? "Tool completed" : "Tool failed"
712
+ );
713
+ }
714
+ function logError(log, error, message, context) {
715
+ if (error instanceof Error) {
716
+ log.error(
717
+ {
718
+ err: error,
719
+ ...context
720
+ },
721
+ message
722
+ );
723
+ } else {
724
+ log.error(
725
+ {
726
+ error: String(error),
727
+ ...context
728
+ },
729
+ message
730
+ );
731
+ }
732
+ }
733
+ function sanitizeToolParams(params) {
734
+ const sanitized = {};
735
+ const maxStringLength = 200;
736
+ for (const [key, value] of Object.entries(params)) {
737
+ const lowerKey = key.toLowerCase();
738
+ if (lowerKey.includes("key") || lowerKey.includes("secret") || lowerKey.includes("token") || lowerKey.includes("password")) {
739
+ sanitized[key] = "[REDACTED]";
740
+ continue;
741
+ }
742
+ if (typeof value === "string" && value.length > maxStringLength) {
743
+ sanitized[key] = `${value.substring(0, maxStringLength)}... (${value.length} chars)`;
744
+ continue;
745
+ }
746
+ sanitized[key] = value;
747
+ }
748
+ return sanitized;
749
+ }
750
+ var LOG_LEVEL_MAP, logger;
751
+ var init_logger = __esm({
752
+ "src/utils/logger.ts"() {
753
+ "use strict";
754
+ init_config();
755
+ init_constants();
756
+ LOG_LEVEL_MAP = {
757
+ debug: "debug",
758
+ info: "info",
759
+ warn: "warn",
760
+ error: "error"
761
+ };
762
+ logger = pino(createLoggerOptions());
763
+ }
764
+ });
765
+
766
+ // src/services/prediction.store.ts
767
+ import { randomUUID } from "crypto";
768
+ function createPredictionStore(options) {
769
+ return new PredictionStore(options);
770
+ }
771
+ function formatPredictionStatus(status) {
772
+ const statusLabels = {
773
+ queued: "Queued",
774
+ processing: "Processing",
775
+ completed: "Completed",
776
+ failed: "Failed",
777
+ cancelled: "Cancelled"
778
+ };
779
+ return statusLabels[status];
780
+ }
781
+ var PredictionStore;
782
+ var init_prediction_store = __esm({
783
+ "src/services/prediction.store.ts"() {
784
+ "use strict";
785
+ init_constants();
786
+ init_error_handler();
787
+ init_logger();
788
+ PredictionStore = class {
789
+ predictions = /* @__PURE__ */ new Map();
790
+ maxQueueSize;
791
+ predictionTimeoutMs;
792
+ cleanupAgeMs;
793
+ enableAutoCleanup;
794
+ cleanupIntervalMs;
795
+ log;
796
+ cleanupTimer = null;
797
+ processor = null;
798
+ processingQueue = false;
799
+ /**
800
+ * Creates a new PredictionStore instance.
801
+ *
802
+ * @param options - Store configuration options
803
+ */
804
+ constructor(options = {}) {
805
+ this.maxQueueSize = options.maxQueueSize ?? PREDICTION_QUEUE.MAX_QUEUE_SIZE;
806
+ this.predictionTimeoutMs = options.predictionTimeoutMs ?? PREDICTION_QUEUE.PREDICTION_TIMEOUT_MS;
807
+ this.cleanupAgeMs = options.cleanupAgeMs ?? PREDICTION_QUEUE.CLEANUP_AGE_MS;
808
+ this.enableAutoCleanup = options.enableAutoCleanup ?? true;
809
+ this.cleanupIntervalMs = options.cleanupIntervalMs ?? 60 * 60 * 1e3;
810
+ this.log = options.logger ?? createChildLogger("prediction-store");
811
+ if (this.enableAutoCleanup) {
812
+ this.startAutoCleanup();
813
+ }
814
+ this.log.debug(
815
+ {
816
+ maxQueueSize: this.maxQueueSize,
817
+ predictionTimeoutMs: this.predictionTimeoutMs,
818
+ cleanupAgeMs: this.cleanupAgeMs
819
+ },
820
+ "PredictionStore initialized"
821
+ );
822
+ }
823
+ // ===========================================================================
824
+ // Public Methods - CRUD Operations
825
+ // ===========================================================================
826
+ /**
827
+ * Creates a new prediction and adds it to the queue.
828
+ *
829
+ * @param options - Prediction creation options
830
+ * @returns The created prediction
831
+ * @throws {IdeogramMCPError} If the queue is full
832
+ *
833
+ * @example
834
+ * ```typescript
835
+ * const prediction = store.create({
836
+ * request: {
837
+ * prompt: 'A serene mountain landscape',
838
+ * num_images: 2,
839
+ * },
840
+ * type: 'generate',
841
+ * });
842
+ * console.log(`Prediction ID: ${prediction.id}`);
843
+ * ```
844
+ */
845
+ create(options) {
846
+ const queuedCount = this.getQueuedCount();
847
+ if (queuedCount >= this.maxQueueSize) {
848
+ throw createInternalError(
849
+ `Queue is full (max ${this.maxQueueSize} predictions). Try again later.`
850
+ );
851
+ }
852
+ const now = (/* @__PURE__ */ new Date()).toISOString();
853
+ const id = this.generatePredictionId();
854
+ const prediction = {
855
+ id,
856
+ status: "queued",
857
+ request: options.request,
858
+ type: options.type,
859
+ created_at: now,
860
+ progress: 0,
861
+ eta_seconds: this.estimateEta(options.request)
862
+ };
863
+ this.predictions.set(id, prediction);
864
+ this.log.info(
865
+ { predictionId: id, type: options.type },
866
+ "Prediction created and queued"
867
+ );
868
+ this.processNextIfIdle();
869
+ return prediction;
870
+ }
871
+ /**
872
+ * Gets a prediction by ID.
873
+ *
874
+ * @param id - The prediction ID
875
+ * @returns The prediction or undefined if not found
876
+ *
877
+ * @example
878
+ * ```typescript
879
+ * const prediction = store.get('pred_abc123');
880
+ * if (prediction) {
881
+ * console.log(`Status: ${prediction.status}`);
882
+ * }
883
+ * ```
884
+ */
885
+ get(id) {
886
+ return this.predictions.get(id);
887
+ }
888
+ /**
889
+ * Gets a prediction by ID, throwing an error if not found.
890
+ *
891
+ * @param id - The prediction ID
892
+ * @returns The prediction
893
+ * @throws {IdeogramMCPError} If the prediction is not found
894
+ */
895
+ getOrThrow(id) {
896
+ const prediction = this.get(id);
897
+ if (!prediction) {
898
+ throw createPredictionNotFoundError(id);
899
+ }
900
+ return prediction;
901
+ }
902
+ /**
903
+ * Updates a prediction with new data.
904
+ *
905
+ * @param id - The prediction ID
906
+ * @param updates - Fields to update
907
+ * @returns The updated prediction
908
+ * @throws {IdeogramMCPError} If the prediction is not found
909
+ *
910
+ * @example
911
+ * ```typescript
912
+ * const updated = store.update('pred_abc123', {
913
+ * status: 'processing',
914
+ * progress: 50,
915
+ * etaSeconds: 15,
916
+ * });
917
+ * ```
918
+ */
919
+ update(id, updates) {
920
+ const prediction = this.getOrThrow(id);
921
+ if (updates.status !== void 0) {
922
+ prediction.status = updates.status;
923
+ if (updates.status === "processing" && !prediction.started_at) {
924
+ prediction.started_at = (/* @__PURE__ */ new Date()).toISOString();
925
+ }
926
+ if (updates.status === "completed" || updates.status === "failed" || updates.status === "cancelled") {
927
+ prediction.completed_at = (/* @__PURE__ */ new Date()).toISOString();
928
+ }
929
+ }
930
+ if (updates.progress !== void 0) {
931
+ prediction.progress = Math.min(100, Math.max(0, updates.progress));
932
+ }
933
+ if (updates.etaSeconds !== void 0) {
934
+ prediction.eta_seconds = updates.etaSeconds;
935
+ }
936
+ if (updates.result !== void 0) {
937
+ prediction.result = updates.result;
938
+ prediction.status = "completed";
939
+ prediction.progress = 100;
940
+ prediction.eta_seconds = 0;
941
+ prediction.completed_at = (/* @__PURE__ */ new Date()).toISOString();
942
+ }
943
+ if (updates.error !== void 0) {
944
+ prediction.error = updates.error;
945
+ prediction.status = "failed";
946
+ prediction.completed_at = (/* @__PURE__ */ new Date()).toISOString();
947
+ }
948
+ this.predictions.set(id, prediction);
949
+ this.log.debug(
950
+ { predictionId: id, status: prediction.status, progress: prediction.progress },
951
+ "Prediction updated"
952
+ );
953
+ return prediction;
954
+ }
955
+ /**
956
+ * Marks a prediction as processing.
957
+ *
958
+ * @param id - The prediction ID
959
+ * @returns The updated prediction
960
+ */
961
+ markProcessing(id) {
962
+ return this.update(id, { status: "processing", progress: 10 });
963
+ }
964
+ /**
965
+ * Marks a prediction as completed with results.
966
+ *
967
+ * @param id - The prediction ID
968
+ * @param result - The generation/edit result
969
+ * @returns The updated prediction
970
+ */
971
+ markCompleted(id, result) {
972
+ const prediction = this.update(id, { result });
973
+ this.log.info(
974
+ { predictionId: id, imageCount: result.data.length },
975
+ "Prediction completed successfully"
976
+ );
977
+ this.processNextIfIdle();
978
+ return prediction;
979
+ }
980
+ /**
981
+ * Marks a prediction as failed with error information.
982
+ *
983
+ * @param id - The prediction ID
984
+ * @param error - Error information
985
+ * @returns The updated prediction
986
+ */
987
+ markFailed(id, error) {
988
+ const prediction = this.update(id, { error });
989
+ this.log.warn(
990
+ { predictionId: id, errorCode: error.code, errorMessage: error.message },
991
+ "Prediction failed"
992
+ );
993
+ this.processNextIfIdle();
994
+ return prediction;
995
+ }
996
+ /**
997
+ * Cancels a prediction if it's still queued.
998
+ * Predictions already processing or completed cannot be cancelled.
999
+ *
1000
+ * @param id - The prediction ID
1001
+ * @returns Object indicating success and current status
1002
+ * @throws {IdeogramMCPError} If the prediction is not found
1003
+ *
1004
+ * @example
1005
+ * ```typescript
1006
+ * const result = store.cancel('pred_abc123');
1007
+ * if (result.success) {
1008
+ * console.log('Prediction cancelled');
1009
+ * } else {
1010
+ * console.log(`Cannot cancel: ${result.status}`);
1011
+ * }
1012
+ * ```
1013
+ */
1014
+ cancel(id) {
1015
+ const prediction = this.getOrThrow(id);
1016
+ if (prediction.status !== "queued") {
1017
+ const statusMessages = {
1018
+ queued: "Prediction is queued",
1019
+ processing: "Prediction is already being processed by the Ideogram API",
1020
+ completed: "Prediction has already completed",
1021
+ failed: "Prediction has already failed",
1022
+ cancelled: "Prediction was already cancelled"
1023
+ };
1024
+ return {
1025
+ success: false,
1026
+ status: prediction.status,
1027
+ message: statusMessages[prediction.status]
1028
+ };
1029
+ }
1030
+ prediction.status = "cancelled";
1031
+ prediction.completed_at = (/* @__PURE__ */ new Date()).toISOString();
1032
+ this.predictions.set(id, prediction);
1033
+ this.log.info({ predictionId: id }, "Prediction cancelled");
1034
+ return {
1035
+ success: true,
1036
+ status: "cancelled",
1037
+ message: "Prediction successfully cancelled"
1038
+ };
1039
+ }
1040
+ /**
1041
+ * Deletes a prediction from the store.
1042
+ *
1043
+ * @param id - The prediction ID
1044
+ * @returns True if deleted, false if not found
1045
+ */
1046
+ delete(id) {
1047
+ const existed = this.predictions.has(id);
1048
+ this.predictions.delete(id);
1049
+ if (existed) {
1050
+ this.log.debug({ predictionId: id }, "Prediction deleted");
1051
+ }
1052
+ return existed;
1053
+ }
1054
+ // ===========================================================================
1055
+ // Public Methods - Query Operations
1056
+ // ===========================================================================
1057
+ /**
1058
+ * Gets all predictions.
1059
+ *
1060
+ * @returns Array of all predictions
1061
+ */
1062
+ getAll() {
1063
+ return Array.from(this.predictions.values());
1064
+ }
1065
+ /**
1066
+ * Gets predictions by status.
1067
+ *
1068
+ * @param status - The status to filter by
1069
+ * @returns Array of matching predictions
1070
+ */
1071
+ getByStatus(status) {
1072
+ return this.getAll().filter((p) => p.status === status);
1073
+ }
1074
+ /**
1075
+ * Gets the next queued prediction (FIFO).
1076
+ *
1077
+ * @returns The oldest queued prediction or undefined
1078
+ */
1079
+ getNextQueued() {
1080
+ const queued = this.getByStatus("queued");
1081
+ if (queued.length === 0) {
1082
+ return void 0;
1083
+ }
1084
+ return queued.sort(
1085
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
1086
+ )[0];
1087
+ }
1088
+ /**
1089
+ * Gets store statistics.
1090
+ *
1091
+ * @returns Statistics about the store contents
1092
+ *
1093
+ * @example
1094
+ * ```typescript
1095
+ * const stats = store.getStats();
1096
+ * console.log(`Queued: ${stats.queued}, Processing: ${stats.processing}`);
1097
+ * ```
1098
+ */
1099
+ getStats() {
1100
+ const all = this.getAll();
1101
+ return {
1102
+ total: all.length,
1103
+ queued: all.filter((p) => p.status === "queued").length,
1104
+ processing: all.filter((p) => p.status === "processing").length,
1105
+ completed: all.filter((p) => p.status === "completed").length,
1106
+ failed: all.filter((p) => p.status === "failed").length,
1107
+ cancelled: all.filter((p) => p.status === "cancelled").length
1108
+ };
1109
+ }
1110
+ /**
1111
+ * Checks if a prediction exists.
1112
+ *
1113
+ * @param id - The prediction ID
1114
+ * @returns True if the prediction exists
1115
+ */
1116
+ has(id) {
1117
+ return this.predictions.has(id);
1118
+ }
1119
+ /**
1120
+ * Gets the count of queued predictions.
1121
+ */
1122
+ getQueuedCount() {
1123
+ return this.getByStatus("queued").length;
1124
+ }
1125
+ /**
1126
+ * Gets the count of processing predictions.
1127
+ */
1128
+ getProcessingCount() {
1129
+ return this.getByStatus("processing").length;
1130
+ }
1131
+ // ===========================================================================
1132
+ // Public Methods - Processing
1133
+ // ===========================================================================
1134
+ /**
1135
+ * Registers a processor function for handling predictions.
1136
+ * When a processor is registered, queued predictions will be processed automatically.
1137
+ *
1138
+ * @param processor - Function that processes a prediction and returns the result
1139
+ *
1140
+ * @example
1141
+ * ```typescript
1142
+ * store.setProcessor(async (prediction) => {
1143
+ * // Call Ideogram API here
1144
+ * return await client.generate(prediction.request);
1145
+ * });
1146
+ * ```
1147
+ */
1148
+ setProcessor(processor) {
1149
+ this.processor = processor;
1150
+ this.log.debug("Processor registered");
1151
+ this.processNextIfIdle();
1152
+ }
1153
+ /**
1154
+ * Processes the next queued prediction if one is available and not already processing.
1155
+ * This is called automatically when predictions are created or completed.
1156
+ */
1157
+ async processNextIfIdle() {
1158
+ if (!this.processor || this.processingQueue) {
1159
+ return;
1160
+ }
1161
+ const next = this.getNextQueued();
1162
+ if (!next) {
1163
+ return;
1164
+ }
1165
+ this.processingQueue = true;
1166
+ try {
1167
+ await this.processOne(next);
1168
+ } finally {
1169
+ this.processingQueue = false;
1170
+ if (this.getQueuedCount() > 0) {
1171
+ setImmediate(() => this.processNextIfIdle());
1172
+ }
1173
+ }
1174
+ }
1175
+ // ===========================================================================
1176
+ // Public Methods - Cleanup
1177
+ // ===========================================================================
1178
+ /**
1179
+ * Cleans up old completed/failed/cancelled predictions.
1180
+ *
1181
+ * @returns Number of predictions removed
1182
+ */
1183
+ cleanup() {
1184
+ const now = Date.now();
1185
+ const cutoff = now - this.cleanupAgeMs;
1186
+ let removed = 0;
1187
+ for (const [id, prediction] of this.predictions) {
1188
+ if (prediction.status !== "completed" && prediction.status !== "failed" && prediction.status !== "cancelled") {
1189
+ continue;
1190
+ }
1191
+ const completedAt = prediction.completed_at ? new Date(prediction.completed_at).getTime() : 0;
1192
+ if (completedAt < cutoff) {
1193
+ this.predictions.delete(id);
1194
+ removed++;
1195
+ }
1196
+ }
1197
+ if (removed > 0) {
1198
+ this.log.info({ removedCount: removed }, "Cleaned up old predictions");
1199
+ }
1200
+ return removed;
1201
+ }
1202
+ /**
1203
+ * Clears all predictions from the store.
1204
+ */
1205
+ clear() {
1206
+ const count = this.predictions.size;
1207
+ this.predictions.clear();
1208
+ this.log.info({ count }, "All predictions cleared");
1209
+ }
1210
+ /**
1211
+ * Stops the automatic cleanup timer.
1212
+ */
1213
+ stopAutoCleanup() {
1214
+ if (this.cleanupTimer) {
1215
+ clearInterval(this.cleanupTimer);
1216
+ this.cleanupTimer = null;
1217
+ this.log.debug("Auto cleanup stopped");
1218
+ }
1219
+ }
1220
+ /**
1221
+ * Disposes the store, stopping timers and clearing data.
1222
+ */
1223
+ dispose() {
1224
+ this.stopAutoCleanup();
1225
+ this.clear();
1226
+ this.processor = null;
1227
+ this.log.debug("PredictionStore disposed");
1228
+ }
1229
+ // ===========================================================================
1230
+ // Private Methods
1231
+ // ===========================================================================
1232
+ /**
1233
+ * Generates a unique prediction ID.
1234
+ */
1235
+ generatePredictionId() {
1236
+ return `pred_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
1237
+ }
1238
+ /**
1239
+ * Estimates the ETA for a prediction based on request parameters.
1240
+ */
1241
+ estimateEta(request) {
1242
+ let eta = 30;
1243
+ const numImages = request.num_images ?? 1;
1244
+ eta += (numImages - 1) * 10;
1245
+ const renderingSpeed = request.rendering_speed ?? "DEFAULT";
1246
+ const speedMultipliers = {
1247
+ FLASH: 0.5,
1248
+ TURBO: 0.75,
1249
+ DEFAULT: 1,
1250
+ QUALITY: 2
1251
+ };
1252
+ eta *= speedMultipliers[renderingSpeed] ?? 1;
1253
+ return Math.round(eta);
1254
+ }
1255
+ /**
1256
+ * Processes a single prediction.
1257
+ */
1258
+ async processOne(prediction) {
1259
+ if (!this.processor) {
1260
+ return;
1261
+ }
1262
+ const { id } = prediction;
1263
+ try {
1264
+ this.markProcessing(id);
1265
+ const progressInterval = setInterval(() => {
1266
+ const current = this.get(id);
1267
+ if (current && current.status === "processing" && (current.progress ?? 0) < 90) {
1268
+ this.update(id, { progress: (current.progress ?? 0) + 20 });
1269
+ }
1270
+ }, 5e3);
1271
+ try {
1272
+ const result = await this.processor(prediction);
1273
+ this.markCompleted(id, result);
1274
+ } finally {
1275
+ clearInterval(progressInterval);
1276
+ }
1277
+ } catch (error) {
1278
+ const errorInfo = this.extractErrorInfo(error);
1279
+ this.markFailed(id, errorInfo);
1280
+ }
1281
+ }
1282
+ /**
1283
+ * Extracts error information from an unknown error.
1284
+ */
1285
+ extractErrorInfo(error) {
1286
+ if (error instanceof IdeogramMCPError) {
1287
+ return {
1288
+ code: error.code,
1289
+ message: error.userMessage,
1290
+ retryable: error.retryable
1291
+ };
1292
+ }
1293
+ if (error instanceof Error) {
1294
+ return {
1295
+ code: "PROCESSING_ERROR",
1296
+ message: error.message,
1297
+ retryable: false
1298
+ };
1299
+ }
1300
+ return {
1301
+ code: "UNKNOWN_ERROR",
1302
+ message: "An unexpected error occurred",
1303
+ retryable: false
1304
+ };
1305
+ }
1306
+ /**
1307
+ * Starts the automatic cleanup timer.
1308
+ */
1309
+ startAutoCleanup() {
1310
+ this.cleanupTimer = setInterval(() => {
1311
+ this.cleanup();
1312
+ }, this.cleanupIntervalMs);
1313
+ if (this.cleanupTimer.unref) {
1314
+ this.cleanupTimer.unref();
1315
+ }
1316
+ this.log.debug(
1317
+ { intervalMs: this.cleanupIntervalMs },
1318
+ "Auto cleanup started"
1319
+ );
1320
+ }
1321
+ };
1322
+ }
1323
+ });
1324
+
1325
+ // src/types/tool.types.ts
1326
+ import { z as z2 } from "zod";
1327
+ var AspectRatioSchema, RenderingSpeedSchema, MagicPromptSchema, StyleTypeSchema, ModelSchema, PredictionStatusSchema, GenerateInputSchema, GenerateAsyncInputSchema, EditInputSchema, GetPredictionInputSchema, CancelPredictionInputSchema;
1328
+ var init_tool_types = __esm({
1329
+ "src/types/tool.types.ts"() {
1330
+ "use strict";
1331
+ AspectRatioSchema = z2.enum([
1332
+ "1x1",
1333
+ "16x9",
1334
+ "9x16",
1335
+ "4x3",
1336
+ "3x4",
1337
+ "3x2",
1338
+ "2x3",
1339
+ "4x5",
1340
+ "5x4",
1341
+ "1x2",
1342
+ "2x1",
1343
+ "1x3",
1344
+ "3x1",
1345
+ "10x16",
1346
+ "16x10"
1347
+ ]);
1348
+ RenderingSpeedSchema = z2.enum([
1349
+ "FLASH",
1350
+ "TURBO",
1351
+ "DEFAULT",
1352
+ "QUALITY"
1353
+ ]);
1354
+ MagicPromptSchema = z2.enum(["AUTO", "ON", "OFF"]);
1355
+ StyleTypeSchema = z2.enum([
1356
+ "AUTO",
1357
+ "GENERAL",
1358
+ "REALISTIC",
1359
+ "DESIGN",
1360
+ "FICTION",
1361
+ "RENDER_3D",
1362
+ "ANIME"
1363
+ ]);
1364
+ ModelSchema = z2.enum(["V_2", "V_2_TURBO"]);
1365
+ PredictionStatusSchema = z2.enum([
1366
+ "queued",
1367
+ "processing",
1368
+ "completed",
1369
+ "failed",
1370
+ "cancelled"
1371
+ ]);
1372
+ GenerateInputSchema = z2.object({
1373
+ /** Text prompt describing the desired image (1-10000 characters) */
1374
+ prompt: z2.string().min(1, "Prompt is required").max(1e4, "Prompt must be 10000 characters or less"),
1375
+ /** Negative prompt to guide what not to include */
1376
+ negative_prompt: z2.string().max(1e4, "Negative prompt must be 10000 characters or less").optional(),
1377
+ /** Aspect ratio for the generated image */
1378
+ aspect_ratio: AspectRatioSchema.optional().default("1x1"),
1379
+ /** Number of images to generate (1-8) */
1380
+ num_images: z2.number().int("Number of images must be an integer").min(1, "Must generate at least 1 image").max(8, "Cannot generate more than 8 images").optional().default(1),
1381
+ /** Random seed for reproducible generation (0-2147483647) */
1382
+ seed: z2.number().int("Seed must be an integer").min(0, "Seed must be non-negative").max(2147483647, "Seed must be at most 2147483647").optional(),
1383
+ /** Rendering speed/quality tradeoff */
1384
+ rendering_speed: RenderingSpeedSchema.optional().default("DEFAULT"),
1385
+ /** Magic prompt enhancement option */
1386
+ magic_prompt: MagicPromptSchema.optional().default("AUTO"),
1387
+ /** Style type for the image */
1388
+ style_type: StyleTypeSchema.optional().default("AUTO"),
1389
+ /** Whether to save generated images locally */
1390
+ save_locally: z2.boolean().optional().default(true)
1391
+ });
1392
+ GenerateAsyncInputSchema = GenerateInputSchema.extend({
1393
+ /** Optional webhook URL for completion notification (reserved for future use) */
1394
+ webhook_url: z2.string().url("Invalid webhook URL").optional()
1395
+ });
1396
+ EditInputSchema = z2.object({
1397
+ /** Text prompt describing the desired changes */
1398
+ prompt: z2.string().min(1, "Prompt is required").max(1e4, "Prompt must be 10000 characters or less"),
1399
+ /** Source image: URL, file path, or base64 data URL */
1400
+ image: z2.string().min(1, "Image is required"),
1401
+ /**
1402
+ * Mask image for inpainting: black=edit, white=preserve
1403
+ * REQUIRED for inpainting
1404
+ */
1405
+ mask: z2.string().min(1, "Mask is required"),
1406
+ /** Model to use for editing (V_2 or V_2_TURBO) */
1407
+ model: ModelSchema.optional().default("V_2"),
1408
+ /** Number of images to generate (1-8) */
1409
+ num_images: z2.number().int("Number of images must be an integer").min(1, "Must generate at least 1 image").max(8, "Cannot generate more than 8 images").optional().default(1),
1410
+ /** Random seed for reproducible generation (0-2147483647) */
1411
+ seed: z2.number().int("Seed must be an integer").min(0, "Seed must be non-negative").max(2147483647, "Seed must be at most 2147483647").optional(),
1412
+ /** Magic prompt enhancement option */
1413
+ magic_prompt: MagicPromptSchema.optional().default("AUTO"),
1414
+ /** Style type for the image */
1415
+ style_type: StyleTypeSchema.optional().default("AUTO"),
1416
+ /** Whether to save generated images locally */
1417
+ save_locally: z2.boolean().optional().default(true)
1418
+ });
1419
+ GetPredictionInputSchema = z2.object({
1420
+ /** Unique prediction ID returned from ideogram_generate_async */
1421
+ prediction_id: z2.string().min(1, "Prediction ID is required")
1422
+ });
1423
+ CancelPredictionInputSchema = z2.object({
1424
+ /** Unique prediction ID to cancel */
1425
+ prediction_id: z2.string().min(1, "Prediction ID is required")
1426
+ });
1427
+ }
1428
+ });
1429
+
1430
+ // src/utils/retry.ts
1431
+ function exponentialBackoff(attempt, options = {}) {
1432
+ const {
1433
+ initialDelayMs = RETRY_CONFIG.INITIAL_DELAY_MS,
1434
+ maxDelayMs = RETRY_CONFIG.MAX_DELAY_MS,
1435
+ backoffMultiplier = RETRY_CONFIG.BACKOFF_MULTIPLIER,
1436
+ jitter = true
1437
+ } = options;
1438
+ const baseDelay = initialDelayMs * Math.pow(backoffMultiplier, attempt);
1439
+ let delay = baseDelay;
1440
+ if (jitter) {
1441
+ const jitterFactor = 0.75 + Math.random() * 0.5;
1442
+ delay = baseDelay * jitterFactor;
1443
+ }
1444
+ return Math.min(Math.round(delay), maxDelayMs);
1445
+ }
1446
+ function calculateRetryDelay(attempt, headers, options = {}) {
1447
+ const retryAfterSeconds = extractRetryAfter(headers);
1448
+ if (retryAfterSeconds !== void 0 && retryAfterSeconds > 0) {
1449
+ const retryAfterMs = retryAfterSeconds * 1e3;
1450
+ const maxDelayMs = options.maxDelayMs ?? RETRY_CONFIG.MAX_DELAY_MS;
1451
+ return Math.min(retryAfterMs, maxDelayMs);
1452
+ }
1453
+ return exponentialBackoff(attempt, options);
1454
+ }
1455
+ async function withRetry(operation, options = {}) {
1456
+ const {
1457
+ maxAttempts = RETRY_CONFIG.MAX_ATTEMPTS,
1458
+ initialDelayMs = RETRY_CONFIG.INITIAL_DELAY_MS,
1459
+ maxDelayMs = RETRY_CONFIG.MAX_DELAY_MS,
1460
+ backoffMultiplier = RETRY_CONFIG.BACKOFF_MULTIPLIER,
1461
+ jitter = true,
1462
+ shouldRetry = defaultShouldRetry,
1463
+ logger: customLogger,
1464
+ onRetry,
1465
+ operationName = "operation"
1466
+ } = options;
1467
+ const log = customLogger ?? createChildLogger("ideogram-client");
1468
+ const totalAttempts = maxAttempts + 1;
1469
+ let lastError;
1470
+ let lastHeaders;
1471
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
1472
+ const context = {
1473
+ attempt,
1474
+ maxAttempts: totalAttempts,
1475
+ isRetry: attempt > 1
1476
+ };
1477
+ try {
1478
+ const result = await operation(context);
1479
+ if (attempt > 1) {
1480
+ log.info(
1481
+ { operation: operationName, attempt, totalAttempts },
1482
+ "Operation succeeded after retry"
1483
+ );
1484
+ }
1485
+ return result;
1486
+ } catch (error) {
1487
+ lastError = error;
1488
+ lastHeaders = extractHeadersFromError(error);
1489
+ const canRetry = attempt < totalAttempts && shouldRetry(error, attempt);
1490
+ if (!canRetry) {
1491
+ log.warn(
1492
+ {
1493
+ operation: operationName,
1494
+ attempt,
1495
+ totalAttempts,
1496
+ error: formatErrorForLog(error),
1497
+ willRetry: false
1498
+ },
1499
+ `Operation failed after ${attempt} attempt(s)`
1500
+ );
1501
+ throw error;
1502
+ }
1503
+ const delayMs = calculateRetryDelay(attempt - 1, lastHeaders, {
1504
+ initialDelayMs,
1505
+ maxDelayMs,
1506
+ backoffMultiplier,
1507
+ jitter
1508
+ });
1509
+ log.debug(
1510
+ {
1511
+ operation: operationName,
1512
+ attempt,
1513
+ totalAttempts,
1514
+ error: formatErrorForLog(error),
1515
+ delayMs,
1516
+ willRetry: true
1517
+ },
1518
+ `Retrying ${operationName} after ${delayMs}ms`
1519
+ );
1520
+ if (onRetry) {
1521
+ onRetry(error, attempt, delayMs);
1522
+ }
1523
+ await sleep(delayMs);
1524
+ }
1525
+ }
1526
+ throw lastError;
1527
+ }
1528
+ function defaultShouldRetry(error, _attempt) {
1529
+ if (isRetryableError(error)) {
1530
+ return true;
1531
+ }
1532
+ if (typeof error === "object" && error !== null) {
1533
+ const err = error;
1534
+ if ("response" in err && err["response"] === void 0) {
1535
+ const code = "code" in err ? err["code"] : void 0;
1536
+ if (code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "EAI_AGAIN") {
1537
+ return true;
1538
+ }
1539
+ }
1540
+ if ("response" in err && typeof err["response"] === "object" && err["response"] !== null) {
1541
+ const response = err["response"];
1542
+ const status = response["status"];
1543
+ if (typeof status === "number" && RETRYABLE_STATUS_CODES.includes(status)) {
1544
+ return true;
1545
+ }
1546
+ }
1547
+ }
1548
+ return false;
1549
+ }
1550
+ function extractHeadersFromError(error) {
1551
+ if (typeof error !== "object" || error === null) {
1552
+ return void 0;
1553
+ }
1554
+ const err = error;
1555
+ if ("response" in err && typeof err["response"] === "object" && err["response"] !== null) {
1556
+ const response = err["response"];
1557
+ if ("headers" in response && typeof response["headers"] === "object") {
1558
+ return response["headers"];
1559
+ }
1560
+ }
1561
+ if (isIdeogramMCPError(error) && error.details) {
1562
+ if ("headers" in error.details && typeof error.details["headers"] === "object") {
1563
+ return error.details["headers"];
1564
+ }
1565
+ }
1566
+ return void 0;
1567
+ }
1568
+ function formatErrorForLog(error) {
1569
+ if (isIdeogramMCPError(error)) {
1570
+ return {
1571
+ code: error.code,
1572
+ message: error.message,
1573
+ statusCode: error.statusCode,
1574
+ retryable: error.retryable
1575
+ };
1576
+ }
1577
+ if (error instanceof Error) {
1578
+ return {
1579
+ name: error.name,
1580
+ message: error.message
1581
+ };
1582
+ }
1583
+ return { error: String(error) };
1584
+ }
1585
+ function sleep(ms) {
1586
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1587
+ }
1588
+ var init_retry = __esm({
1589
+ "src/utils/retry.ts"() {
1590
+ "use strict";
1591
+ init_constants();
1592
+ init_error_handler();
1593
+ init_logger();
1594
+ }
1595
+ });
1596
+
1597
+ // src/services/ideogram.client.ts
1598
+ import axios from "axios";
1599
+ import FormData from "form-data";
1600
+ function createIdeogramClient(options) {
1601
+ return new IdeogramClient(options);
1602
+ }
1603
+ var IdeogramClient;
1604
+ var init_ideogram_client = __esm({
1605
+ "src/services/ideogram.client.ts"() {
1606
+ "use strict";
1607
+ init_constants();
1608
+ init_config();
1609
+ init_error_handler();
1610
+ init_retry();
1611
+ init_logger();
1612
+ IdeogramClient = class {
1613
+ apiKey;
1614
+ baseUrl;
1615
+ timeoutMs;
1616
+ longTimeoutMs;
1617
+ retryOptions;
1618
+ log;
1619
+ httpClient;
1620
+ /**
1621
+ * Creates a new IdeogramClient instance.
1622
+ *
1623
+ * @param options - Client configuration options
1624
+ * @throws {IdeogramMCPError} If no API key is provided or found in environment
1625
+ */
1626
+ constructor(options = {}) {
1627
+ const apiKey = options.apiKey ?? config.ideogramApiKey;
1628
+ if (!apiKey) {
1629
+ throw createMissingApiKeyError();
1630
+ }
1631
+ this.apiKey = apiKey;
1632
+ this.baseUrl = options.baseUrl ?? API_BASE_URL;
1633
+ this.timeoutMs = options.timeoutMs ?? config.requestTimeoutMs ?? TIMEOUTS.DEFAULT_REQUEST_MS;
1634
+ this.longTimeoutMs = options.longTimeoutMs ?? TIMEOUTS.LONG_REQUEST_MS;
1635
+ this.retryOptions = options.retryOptions ?? {};
1636
+ this.log = options.logger ?? createChildLogger("ideogram-client");
1637
+ this.httpClient = axios.create({
1638
+ baseURL: this.baseUrl,
1639
+ timeout: this.timeoutMs,
1640
+ validateStatus: (status) => status >= 200 && status < 300
1641
+ });
1642
+ this.log.debug(
1643
+ { baseUrl: this.baseUrl, timeoutMs: this.timeoutMs },
1644
+ "IdeogramClient initialized"
1645
+ );
1646
+ }
1647
+ // ===========================================================================
1648
+ // Public Methods
1649
+ // ===========================================================================
1650
+ /**
1651
+ * Generates images from a text prompt using Ideogram V3.
1652
+ *
1653
+ * @param params - Generation parameters
1654
+ * @returns Promise resolving to the generation response with image URLs
1655
+ * @throws {IdeogramMCPError} On validation errors, API errors, or network failures
1656
+ *
1657
+ * @example
1658
+ * ```typescript
1659
+ * const result = await client.generate({
1660
+ * prompt: 'A cute cat wearing a wizard hat',
1661
+ * aspectRatio: '1x1',
1662
+ * numImages: 2,
1663
+ * renderingSpeed: 'QUALITY',
1664
+ * });
1665
+ *
1666
+ * console.log(result.data[0].url); // Temporary URL to generated image
1667
+ * ```
1668
+ */
1669
+ async generate(params) {
1670
+ const endpoint = API_ENDPOINTS.GENERATE_V3;
1671
+ const startTime = Date.now();
1672
+ const imageRequest = {
1673
+ prompt: params.prompt,
1674
+ num_images: params.numImages ?? DEFAULTS.NUM_IMAGES,
1675
+ rendering_speed: params.renderingSpeed ?? DEFAULTS.RENDERING_SPEED,
1676
+ magic_prompt: params.magicPrompt ?? DEFAULTS.MAGIC_PROMPT,
1677
+ style_type: params.styleType ?? DEFAULTS.STYLE_TYPE
1678
+ };
1679
+ const normalizedAspectRatio = this.normalizeAspectRatio(params.aspectRatio);
1680
+ if (normalizedAspectRatio !== void 0) {
1681
+ imageRequest.aspect_ratio = normalizedAspectRatio;
1682
+ }
1683
+ if (params.negativePrompt !== void 0) {
1684
+ imageRequest.negative_prompt = params.negativePrompt;
1685
+ }
1686
+ if (params.seed !== void 0) {
1687
+ imageRequest.seed = params.seed;
1688
+ }
1689
+ const requestBody = imageRequest;
1690
+ const requestContext = {
1691
+ endpoint,
1692
+ method: "POST",
1693
+ hasImage: false,
1694
+ hasMask: false
1695
+ };
1696
+ logApiRequest(this.log, requestContext);
1697
+ const timeout = this.getTimeoutForRenderingSpeed(params.renderingSpeed);
1698
+ const response = await this.executeWithRetry(
1699
+ endpoint,
1700
+ requestBody,
1701
+ timeout,
1702
+ "generate"
1703
+ );
1704
+ const responseContext = {
1705
+ endpoint,
1706
+ statusCode: 200,
1707
+ durationMs: Date.now() - startTime,
1708
+ imageCount: response.data.length
1709
+ };
1710
+ logApiResponse(this.log, responseContext);
1711
+ return response;
1712
+ }
1713
+ /**
1714
+ * Edits an existing image using inpainting.
1715
+ *
1716
+ * Inpainting uses a mask to define which areas to edit:
1717
+ * - Black pixels in mask = areas to modify
1718
+ * - White pixels in mask = areas to preserve
1719
+ *
1720
+ * @param params - Edit parameters including image, mask, and prompt
1721
+ * @returns Promise resolving to the edit response with image URLs
1722
+ * @throws {IdeogramMCPError} On validation errors, API errors, or network failures
1723
+ *
1724
+ * @example
1725
+ * ```typescript
1726
+ * const result = await client.edit({
1727
+ * prompt: 'Add a red balloon in the sky',
1728
+ * image: 'https://example.com/photo.jpg',
1729
+ * mask: maskBuffer, // Black=edit, white=preserve
1730
+ * model: 'V_2',
1731
+ * magicPrompt: 'AUTO',
1732
+ * });
1733
+ *
1734
+ * console.log(result.data[0].url); // Edited image URL
1735
+ * ```
1736
+ */
1737
+ async edit(params) {
1738
+ const endpoint = API_ENDPOINTS.EDIT_LEGACY;
1739
+ const startTime = Date.now();
1740
+ const preparedImage = await this.prepareImage(params.image, "image");
1741
+ const preparedMask = await this.prepareImage(params.mask, "mask");
1742
+ const formData = new FormData();
1743
+ formData.append("prompt", params.prompt);
1744
+ formData.append("model", params.model ?? "V_2");
1745
+ formData.append("magic_prompt_option", params.magicPrompt ?? DEFAULTS.MAGIC_PROMPT);
1746
+ formData.append("num_images", String(params.numImages ?? DEFAULTS.NUM_IMAGES));
1747
+ if (params.seed !== void 0) {
1748
+ formData.append("seed", String(params.seed));
1749
+ }
1750
+ if (params.styleType !== void 0) {
1751
+ formData.append("style_type", params.styleType);
1752
+ }
1753
+ formData.append("image_file", preparedImage.data, {
1754
+ contentType: preparedImage.contentType,
1755
+ filename: preparedImage.filename
1756
+ });
1757
+ formData.append("mask", preparedMask.data, {
1758
+ contentType: preparedMask.contentType,
1759
+ filename: preparedMask.filename
1760
+ });
1761
+ const requestContext = {
1762
+ endpoint,
1763
+ method: "POST",
1764
+ hasImage: true,
1765
+ hasMask: true
1766
+ };
1767
+ logApiRequest(this.log, requestContext);
1768
+ const timeout = TIMEOUTS.LONG_REQUEST_MS;
1769
+ const response = await this.executeWithRetry(
1770
+ endpoint,
1771
+ formData,
1772
+ timeout,
1773
+ "edit"
1774
+ );
1775
+ const responseContext = {
1776
+ endpoint,
1777
+ statusCode: 200,
1778
+ durationMs: Date.now() - startTime,
1779
+ imageCount: response.data.length
1780
+ };
1781
+ logApiResponse(this.log, responseContext);
1782
+ return response;
1783
+ }
1784
+ /**
1785
+ * Gets the current API key (masked for security).
1786
+ */
1787
+ getMaskedApiKey() {
1788
+ if (this.apiKey.length <= 8) {
1789
+ return "****";
1790
+ }
1791
+ return `${this.apiKey.slice(0, 4)}...${this.apiKey.slice(-4)}`;
1792
+ }
1793
+ /**
1794
+ * Tests the API connection by checking if the API key is valid.
1795
+ * Makes a minimal request to verify authentication.
1796
+ *
1797
+ * @returns Promise resolving to true if the connection is valid
1798
+ * @throws {IdeogramMCPError} If the API key is invalid or there's a network error
1799
+ */
1800
+ async testConnection() {
1801
+ try {
1802
+ await this.generate({
1803
+ prompt: "test",
1804
+ numImages: 1,
1805
+ renderingSpeed: "FLASH"
1806
+ });
1807
+ return true;
1808
+ } catch (error) {
1809
+ throw wrapError(error);
1810
+ }
1811
+ }
1812
+ // ===========================================================================
1813
+ // Private Methods
1814
+ // ===========================================================================
1815
+ /**
1816
+ * Executes an API request with automatic retry on transient failures.
1817
+ * Supports both JSON payloads (for generate) and FormData (for edit with images).
1818
+ */
1819
+ async executeWithRetry(endpoint, data, timeout, operationName) {
1820
+ return withRetry(
1821
+ async () => {
1822
+ try {
1823
+ const headers = {
1824
+ [API_KEY_HEADER]: this.apiKey
1825
+ };
1826
+ if (data instanceof FormData) {
1827
+ Object.assign(headers, data.getHeaders());
1828
+ } else {
1829
+ headers["Content-Type"] = "application/json";
1830
+ }
1831
+ const response = await this.httpClient.post(endpoint, data, {
1832
+ headers,
1833
+ timeout
1834
+ });
1835
+ return response.data;
1836
+ } catch (error) {
1837
+ if (axios.isAxiosError(error)) {
1838
+ throw fromAxiosError(this.createAxiosErrorInfo(error));
1839
+ }
1840
+ throw wrapError(error);
1841
+ }
1842
+ },
1843
+ {
1844
+ ...this.retryOptions,
1845
+ operationName,
1846
+ logger: this.log
1847
+ }
1848
+ );
1849
+ }
1850
+ /**
1851
+ * Converts an Axios error to the format expected by fromAxiosError.
1852
+ */
1853
+ createAxiosErrorInfo(error) {
1854
+ const result = {
1855
+ message: error.message
1856
+ };
1857
+ if (error.code !== void 0) {
1858
+ result.code = error.code;
1859
+ }
1860
+ if (error.response) {
1861
+ const responseData = error.response.data;
1862
+ result.response = {
1863
+ status: error.response.status
1864
+ };
1865
+ if (responseData !== void 0) {
1866
+ result.response.data = responseData;
1867
+ }
1868
+ }
1869
+ return result;
1870
+ }
1871
+ /**
1872
+ * Prepares an image input for form upload.
1873
+ * Handles URLs, base64 data URLs, and Buffers.
1874
+ */
1875
+ async prepareImage(input, fieldName) {
1876
+ if (Buffer.isBuffer(input)) {
1877
+ return this.prepareBufferImage(input, fieldName);
1878
+ }
1879
+ if (input.startsWith("data:")) {
1880
+ return this.prepareBase64Image(input, fieldName);
1881
+ }
1882
+ if (input.startsWith("http://") || input.startsWith("https://")) {
1883
+ return this.prepareUrlImage(input, fieldName);
1884
+ }
1885
+ throw createInvalidImageError(
1886
+ `Unsupported image input format for ${fieldName}. Provide a URL, base64 data URL, or Buffer.`
1887
+ );
1888
+ }
1889
+ /**
1890
+ * Prepares a Buffer image for upload.
1891
+ */
1892
+ prepareBufferImage(buffer, fieldName) {
1893
+ const contentType = this.detectImageType(buffer);
1894
+ const extension = this.getExtensionForContentType(contentType);
1895
+ this.validateImageSize(buffer.length);
1896
+ return {
1897
+ data: buffer,
1898
+ contentType,
1899
+ filename: `${fieldName}.${extension}`
1900
+ };
1901
+ }
1902
+ /**
1903
+ * Prepares a base64 data URL image for upload.
1904
+ */
1905
+ prepareBase64Image(dataUrl, fieldName) {
1906
+ const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
1907
+ if (!matches) {
1908
+ throw createInvalidImageError(`Invalid base64 data URL format for ${fieldName}`);
1909
+ }
1910
+ const contentType = matches[1] ?? "image/png";
1911
+ const base64Data = matches[2] ?? "";
1912
+ const buffer = Buffer.from(base64Data, "base64");
1913
+ this.validateImageSize(buffer.length);
1914
+ const extension = this.getExtensionForContentType(contentType);
1915
+ return {
1916
+ data: buffer,
1917
+ contentType,
1918
+ filename: `${fieldName}.${extension}`
1919
+ };
1920
+ }
1921
+ /**
1922
+ * Prepares a URL image for upload by downloading it.
1923
+ */
1924
+ async prepareUrlImage(url, fieldName) {
1925
+ try {
1926
+ const response = await axios.get(url, {
1927
+ responseType: "arraybuffer",
1928
+ timeout: TIMEOUTS.IMAGE_DOWNLOAD_MS
1929
+ });
1930
+ const buffer = Buffer.from(response.data);
1931
+ this.validateImageSize(buffer.length);
1932
+ let contentType = response.headers["content-type"]?.toString().split(";")[0];
1933
+ if (!contentType || !contentType.startsWith("image/")) {
1934
+ contentType = this.detectImageType(buffer);
1935
+ }
1936
+ const extension = this.getExtensionForContentType(contentType);
1937
+ return {
1938
+ data: buffer,
1939
+ contentType,
1940
+ filename: `${fieldName}.${extension}`
1941
+ };
1942
+ } catch (error) {
1943
+ if (axios.isAxiosError(error)) {
1944
+ throw createNetworkError(
1945
+ `Failed to download image from URL: ${error.message}`,
1946
+ error
1947
+ );
1948
+ }
1949
+ throw wrapError(error);
1950
+ }
1951
+ }
1952
+ /**
1953
+ * Validates that the image size is within limits.
1954
+ */
1955
+ validateImageSize(sizeBytes) {
1956
+ const maxSize = 10 * 1024 * 1024;
1957
+ if (sizeBytes > maxSize) {
1958
+ throw createImageTooLargeError(sizeBytes, maxSize);
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Detects the image type from the first few bytes (magic numbers).
1963
+ */
1964
+ detectImageType(buffer) {
1965
+ if (buffer.length < 4) {
1966
+ return "image/png";
1967
+ }
1968
+ if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
1969
+ return "image/png";
1970
+ }
1971
+ if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
1972
+ return "image/jpeg";
1973
+ }
1974
+ if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70 && buffer.length >= 12 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80) {
1975
+ return "image/webp";
1976
+ }
1977
+ return "image/png";
1978
+ }
1979
+ /**
1980
+ * Gets the file extension for a content type.
1981
+ */
1982
+ getExtensionForContentType(contentType) {
1983
+ const extensions = {
1984
+ "image/png": "png",
1985
+ "image/jpeg": "jpg",
1986
+ "image/webp": "webp"
1987
+ };
1988
+ return extensions[contentType] ?? "png";
1989
+ }
1990
+ /**
1991
+ * Normalizes aspect ratio input to the API format.
1992
+ * Converts "16:9" format to "16x9" format if needed.
1993
+ */
1994
+ normalizeAspectRatio(ratio) {
1995
+ if (!ratio) {
1996
+ return void 0;
1997
+ }
1998
+ return ratio.replace(":", "x");
1999
+ }
2000
+ /**
2001
+ * Gets the appropriate timeout based on rendering speed.
2002
+ * Quality rendering may take longer.
2003
+ */
2004
+ getTimeoutForRenderingSpeed(speed) {
2005
+ if (speed === "QUALITY") {
2006
+ return this.longTimeoutMs;
2007
+ }
2008
+ return this.timeoutMs;
2009
+ }
2010
+ };
2011
+ }
2012
+ });
2013
+
2014
+ // src/services/cost.calculator.ts
2015
+ function calculateCost(params = {}) {
2016
+ const numImages = params.numImages ?? DEFAULTS.NUM_IMAGES;
2017
+ const renderingSpeed = params.renderingSpeed ?? DEFAULTS.RENDERING_SPEED;
2018
+ const creditsPerImage = CREDITS_PER_IMAGE[renderingSpeed];
2019
+ const creditsUsed = creditsPerImage * numImages;
2020
+ const estimatedUsd = creditsUsed * USD_PER_CREDIT;
2021
+ return {
2022
+ credits_used: roundCredits(creditsUsed),
2023
+ estimated_usd: roundUsd(estimatedUsd),
2024
+ pricing_tier: renderingSpeed,
2025
+ num_images: numImages
2026
+ };
2027
+ }
2028
+ function calculateEditCost(params = {}) {
2029
+ const numImages = params.numImages ?? DEFAULTS.NUM_IMAGES;
2030
+ const renderingSpeed = params.renderingSpeed ?? DEFAULTS.RENDERING_SPEED;
2031
+ const creditsPerImage = EDIT_CREDITS_PER_IMAGE[renderingSpeed];
2032
+ const creditsUsed = creditsPerImage * numImages;
2033
+ const estimatedUsd = creditsUsed * USD_PER_CREDIT;
2034
+ return {
2035
+ credits_used: roundCredits(creditsUsed),
2036
+ estimated_usd: roundUsd(estimatedUsd),
2037
+ pricing_tier: renderingSpeed,
2038
+ num_images: numImages
2039
+ };
2040
+ }
2041
+ function toCostEstimateOutput(cost) {
2042
+ return {
2043
+ credits_used: cost.credits_used,
2044
+ estimated_usd: cost.estimated_usd,
2045
+ pricing_tier: cost.pricing_tier,
2046
+ num_images: cost.num_images
2047
+ };
2048
+ }
2049
+ function roundCredits(credits) {
2050
+ return Math.round(credits * 100) / 100;
2051
+ }
2052
+ function roundUsd(usd) {
2053
+ return Math.round(usd * 1e4) / 1e4;
2054
+ }
2055
+ var init_cost_calculator = __esm({
2056
+ "src/services/cost.calculator.ts"() {
2057
+ "use strict";
2058
+ init_constants();
2059
+ }
2060
+ });
2061
+
2062
+ // src/services/storage.service.ts
2063
+ import axios2 from "axios";
2064
+ import * as fs from "fs";
2065
+ import * as path from "path";
2066
+ import { randomUUID as randomUUID2 } from "crypto";
2067
+ function createStorageService(options) {
2068
+ return new StorageService(options);
2069
+ }
2070
+ var StorageService;
2071
+ var init_storage_service = __esm({
2072
+ "src/services/storage.service.ts"() {
2073
+ "use strict";
2074
+ init_config();
2075
+ init_constants();
2076
+ init_error_handler();
2077
+ init_logger();
2078
+ StorageService = class {
2079
+ storageDir;
2080
+ enabled;
2081
+ downloadTimeoutMs;
2082
+ log;
2083
+ initialized = false;
2084
+ /**
2085
+ * Creates a new StorageService instance.
2086
+ *
2087
+ * @param options - Service configuration options
2088
+ */
2089
+ constructor(options = {}) {
2090
+ this.storageDir = options.storageDir ?? config.localSaveDir ?? "./ideogram_images";
2091
+ this.enabled = options.enabled ?? config.enableLocalSave ?? true;
2092
+ this.downloadTimeoutMs = options.downloadTimeoutMs ?? TIMEOUTS.IMAGE_DOWNLOAD_MS;
2093
+ this.log = options.logger ?? createChildLogger("storage");
2094
+ this.log.debug(
2095
+ { storageDir: this.storageDir, enabled: this.enabled },
2096
+ "StorageService initialized"
2097
+ );
2098
+ }
2099
+ // ===========================================================================
2100
+ // Public Methods
2101
+ // ===========================================================================
2102
+ /**
2103
+ * Downloads an image from a URL and saves it locally.
2104
+ *
2105
+ * @param url - The URL to download the image from
2106
+ * @param options - Download options
2107
+ * @returns Promise resolving to saved image information
2108
+ * @throws {IdeogramMCPError} If download or save fails
2109
+ *
2110
+ * @example
2111
+ * ```typescript
2112
+ * const result = await storage.downloadImage(
2113
+ * 'https://api.ideogram.ai/temp/image.png',
2114
+ * { prefix: 'generated' }
2115
+ * );
2116
+ * console.log(`Saved: ${result.filename}`);
2117
+ * ```
2118
+ */
2119
+ async downloadImage(url, options = {}) {
2120
+ if (!this.enabled) {
2121
+ throw createStorageError("download", "Local storage is disabled");
2122
+ }
2123
+ await this.ensureStorageDirectory(options.subdir);
2124
+ const startTime = Date.now();
2125
+ this.log.debug({ url }, "Downloading image");
2126
+ try {
2127
+ const response = await axios2.get(url, {
2128
+ responseType: "arraybuffer",
2129
+ timeout: this.downloadTimeoutMs,
2130
+ validateStatus: (status) => status >= 200 && status < 300
2131
+ });
2132
+ const buffer = Buffer.from(response.data);
2133
+ if (buffer.length > VALIDATION.IMAGE.MAX_SIZE_BYTES) {
2134
+ throw createStorageError(
2135
+ "download",
2136
+ `Image size ${(buffer.length / (1024 * 1024)).toFixed(2)}MB exceeds maximum 10MB`
2137
+ );
2138
+ }
2139
+ const mimeType = this.detectImageType(buffer);
2140
+ const extension = this.getExtensionForMimeType(mimeType);
2141
+ const filename = this.generateFilename(options, extension);
2142
+ const targetDir = options.subdir ? path.join(this.storageDir, options.subdir) : this.storageDir;
2143
+ const filePath = path.resolve(targetDir, filename);
2144
+ await this.saveFile(filePath, buffer);
2145
+ const durationMs = Date.now() - startTime;
2146
+ this.log.info(
2147
+ { filename, sizeBytes: buffer.length, durationMs },
2148
+ "Image saved successfully"
2149
+ );
2150
+ return {
2151
+ filePath,
2152
+ filename,
2153
+ originalUrl: url,
2154
+ sizeBytes: buffer.length,
2155
+ mimeType
2156
+ };
2157
+ } catch (error) {
2158
+ if (error instanceof IdeogramMCPError) {
2159
+ throw error;
2160
+ }
2161
+ if (axios2.isAxiosError(error)) {
2162
+ const message = error.response ? `HTTP ${error.response.status}: ${error.message}` : error.message;
2163
+ throw createDownloadFailedError(url, message);
2164
+ }
2165
+ throw wrapError(error);
2166
+ }
2167
+ }
2168
+ /**
2169
+ * Downloads multiple images in parallel.
2170
+ *
2171
+ * @param urls - Array of URLs to download
2172
+ * @param options - Download options applied to all images
2173
+ * @returns Promise resolving to batch save result
2174
+ *
2175
+ * @example
2176
+ * ```typescript
2177
+ * const results = await storage.downloadImages(
2178
+ * ['https://...', 'https://...'],
2179
+ * { prefix: 'batch' }
2180
+ * );
2181
+ * if (results.failureCount > 0) {
2182
+ * console.warn(`${results.failureCount} images failed to download`);
2183
+ * }
2184
+ * ```
2185
+ */
2186
+ async downloadImages(urls, options = {}) {
2187
+ if (!this.enabled) {
2188
+ return {
2189
+ saved: [],
2190
+ failed: urls.map((url) => ({ url, error: "Local storage is disabled" })),
2191
+ total: urls.length,
2192
+ successCount: 0,
2193
+ failureCount: urls.length
2194
+ };
2195
+ }
2196
+ const results = {
2197
+ saved: [],
2198
+ failed: [],
2199
+ total: urls.length,
2200
+ successCount: 0,
2201
+ failureCount: 0
2202
+ };
2203
+ const downloadPromises = urls.map(async (url, index) => {
2204
+ try {
2205
+ const imageOptions = {
2206
+ ...options,
2207
+ // Add index to prefix if multiple images
2208
+ prefix: options.prefix ? `${options.prefix}_${index + 1}` : `image_${index + 1}`
2209
+ };
2210
+ const saved = await this.downloadImage(url, imageOptions);
2211
+ return { success: true, url, saved };
2212
+ } catch (error) {
2213
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2214
+ return { success: false, url, error: errorMessage };
2215
+ }
2216
+ });
2217
+ const downloadResults = await Promise.all(downloadPromises);
2218
+ for (const result of downloadResults) {
2219
+ if (result.success) {
2220
+ results.saved.push(result.saved);
2221
+ results.successCount++;
2222
+ } else {
2223
+ results.failed.push({ url: result.url, error: result.error });
2224
+ results.failureCount++;
2225
+ }
2226
+ }
2227
+ this.log.info(
2228
+ {
2229
+ total: results.total,
2230
+ success: results.successCount,
2231
+ failed: results.failureCount
2232
+ },
2233
+ "Batch download completed"
2234
+ );
2235
+ return results;
2236
+ }
2237
+ /**
2238
+ * Saves raw image data to a file.
2239
+ *
2240
+ * @param data - Image data as Buffer or base64 string
2241
+ * @param options - Save options
2242
+ * @returns Promise resolving to saved image information
2243
+ * @throws {IdeogramMCPError} If save fails
2244
+ *
2245
+ * @example
2246
+ * ```typescript
2247
+ * const imageBuffer = Buffer.from(base64Data, 'base64');
2248
+ * const result = await storage.saveImage(imageBuffer, { prefix: 'edited' });
2249
+ * ```
2250
+ */
2251
+ async saveImage(data, options = {}) {
2252
+ if (!this.enabled) {
2253
+ throw createStorageError("save", "Local storage is disabled");
2254
+ }
2255
+ await this.ensureStorageDirectory(options.subdir);
2256
+ const buffer = typeof data === "string" ? Buffer.from(data, "base64") : data;
2257
+ if (buffer.length > VALIDATION.IMAGE.MAX_SIZE_BYTES) {
2258
+ throw createStorageError(
2259
+ "save",
2260
+ `Image size ${(buffer.length / (1024 * 1024)).toFixed(2)}MB exceeds maximum 10MB`
2261
+ );
2262
+ }
2263
+ const mimeType = this.detectImageType(buffer);
2264
+ const extension = this.getExtensionForMimeType(mimeType);
2265
+ const filename = this.generateFilename(options, extension);
2266
+ const targetDir = options.subdir ? path.join(this.storageDir, options.subdir) : this.storageDir;
2267
+ const filePath = path.resolve(targetDir, filename);
2268
+ await this.saveFile(filePath, buffer);
2269
+ this.log.info({ filename, sizeBytes: buffer.length }, "Image saved");
2270
+ return {
2271
+ filePath,
2272
+ filename,
2273
+ originalUrl: "local",
2274
+ sizeBytes: buffer.length,
2275
+ mimeType
2276
+ };
2277
+ }
2278
+ /**
2279
+ * Checks if local storage is enabled.
2280
+ */
2281
+ isEnabled() {
2282
+ return this.enabled;
2283
+ }
2284
+ /**
2285
+ * Gets the configured storage directory.
2286
+ */
2287
+ getStorageDir() {
2288
+ return path.resolve(this.storageDir);
2289
+ }
2290
+ /**
2291
+ * Checks if a file exists in the storage directory.
2292
+ *
2293
+ * @param filename - The filename to check
2294
+ * @param subdir - Optional subdirectory
2295
+ */
2296
+ async fileExists(filename, subdir) {
2297
+ const targetDir = subdir ? path.join(this.storageDir, subdir) : this.storageDir;
2298
+ const filePath = path.resolve(targetDir, filename);
2299
+ try {
2300
+ await fs.promises.access(filePath, fs.constants.F_OK);
2301
+ return true;
2302
+ } catch {
2303
+ return false;
2304
+ }
2305
+ }
2306
+ /**
2307
+ * Deletes a file from the storage directory.
2308
+ *
2309
+ * @param filename - The filename to delete
2310
+ * @param subdir - Optional subdirectory
2311
+ * @returns True if deleted, false if file didn't exist
2312
+ */
2313
+ async deleteFile(filename, subdir) {
2314
+ const targetDir = subdir ? path.join(this.storageDir, subdir) : this.storageDir;
2315
+ const filePath = path.resolve(targetDir, filename);
2316
+ try {
2317
+ await fs.promises.unlink(filePath);
2318
+ this.log.debug({ filename }, "File deleted");
2319
+ return true;
2320
+ } catch (error) {
2321
+ if (error.code === "ENOENT") {
2322
+ return false;
2323
+ }
2324
+ throw createStorageError("delete", `Failed to delete file: ${filename}`);
2325
+ }
2326
+ }
2327
+ /**
2328
+ * Lists files in the storage directory.
2329
+ *
2330
+ * @param subdir - Optional subdirectory
2331
+ * @returns Array of filenames
2332
+ */
2333
+ async listFiles(subdir) {
2334
+ const targetDir = subdir ? path.join(this.storageDir, subdir) : this.storageDir;
2335
+ const resolvedDir = path.resolve(targetDir);
2336
+ try {
2337
+ const entries = await fs.promises.readdir(resolvedDir, { withFileTypes: true });
2338
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
2339
+ } catch (error) {
2340
+ if (error.code === "ENOENT") {
2341
+ return [];
2342
+ }
2343
+ throw createStorageError("list", "Failed to list storage directory");
2344
+ }
2345
+ }
2346
+ /**
2347
+ * Gets the total size of files in the storage directory.
2348
+ *
2349
+ * @param subdir - Optional subdirectory
2350
+ * @returns Total size in bytes
2351
+ */
2352
+ async getStorageSize(subdir) {
2353
+ const targetDir = subdir ? path.join(this.storageDir, subdir) : this.storageDir;
2354
+ const resolvedDir = path.resolve(targetDir);
2355
+ try {
2356
+ const entries = await fs.promises.readdir(resolvedDir, { withFileTypes: true });
2357
+ let totalSize = 0;
2358
+ for (const entry of entries) {
2359
+ if (entry.isFile()) {
2360
+ const filePath = path.join(resolvedDir, entry.name);
2361
+ const stats = await fs.promises.stat(filePath);
2362
+ totalSize += stats.size;
2363
+ }
2364
+ }
2365
+ return totalSize;
2366
+ } catch (error) {
2367
+ if (error.code === "ENOENT") {
2368
+ return 0;
2369
+ }
2370
+ throw createStorageError("size", "Failed to calculate storage size");
2371
+ }
2372
+ }
2373
+ // ===========================================================================
2374
+ // Private Methods
2375
+ // ===========================================================================
2376
+ /**
2377
+ * Ensures the storage directory exists, creating it if necessary.
2378
+ */
2379
+ async ensureStorageDirectory(subdir) {
2380
+ const targetDir = subdir ? path.join(this.storageDir, subdir) : this.storageDir;
2381
+ const resolvedDir = path.resolve(targetDir);
2382
+ if (this.initialized && !subdir) {
2383
+ return;
2384
+ }
2385
+ try {
2386
+ await fs.promises.mkdir(resolvedDir, { recursive: true });
2387
+ if (!subdir) {
2388
+ this.initialized = true;
2389
+ }
2390
+ this.log.debug({ dir: resolvedDir }, "Storage directory ready");
2391
+ } catch (error) {
2392
+ throw createStorageError(
2393
+ "init",
2394
+ `Failed to create storage directory: ${error.message}`
2395
+ );
2396
+ }
2397
+ }
2398
+ /**
2399
+ * Saves data to a file.
2400
+ */
2401
+ async saveFile(filePath, data) {
2402
+ try {
2403
+ await fs.promises.writeFile(filePath, data);
2404
+ } catch (error) {
2405
+ throw createStorageError(
2406
+ "write",
2407
+ `Failed to write file: ${error.message}`
2408
+ );
2409
+ }
2410
+ }
2411
+ /**
2412
+ * Generates a unique filename for an image.
2413
+ */
2414
+ generateFilename(options, extension) {
2415
+ if (options.filename) {
2416
+ return `${options.filename}.${extension}`;
2417
+ }
2418
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2419
+ const uuid = randomUUID2().slice(0, 8);
2420
+ if (options.prefix) {
2421
+ return `${options.prefix}_${timestamp}_${uuid}.${extension}`;
2422
+ }
2423
+ return `ideogram_${timestamp}_${uuid}.${extension}`;
2424
+ }
2425
+ /**
2426
+ * Detects the image type from the first few bytes (magic numbers).
2427
+ */
2428
+ detectImageType(buffer) {
2429
+ if (buffer.length < 4) {
2430
+ return "image/png";
2431
+ }
2432
+ if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
2433
+ return "image/png";
2434
+ }
2435
+ if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
2436
+ return "image/jpeg";
2437
+ }
2438
+ if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70 && buffer.length >= 12 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80) {
2439
+ return "image/webp";
2440
+ }
2441
+ if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 56) {
2442
+ return "image/gif";
2443
+ }
2444
+ return "image/png";
2445
+ }
2446
+ /**
2447
+ * Gets the file extension for a MIME type.
2448
+ */
2449
+ getExtensionForMimeType(mimeType) {
2450
+ const extensions = {
2451
+ "image/png": "png",
2452
+ "image/jpeg": "jpg",
2453
+ "image/webp": "webp",
2454
+ "image/gif": "gif"
2455
+ };
2456
+ return extensions[mimeType] ?? "png";
2457
+ }
2458
+ };
2459
+ }
2460
+ });
2461
+
2462
+ // src/tools/generate.ts
2463
+ function createGenerateHandler(options = {}) {
2464
+ const log = options.logger ?? createChildLogger("tool:generate");
2465
+ const client = options.client ?? createIdeogramClient(options.clientOptions);
2466
+ const storage = options.storage ?? createStorageService(options.storageOptions);
2467
+ return async function ideogramGenerateHandler(input) {
2468
+ const startTime = Date.now();
2469
+ logToolInvocation(log, {
2470
+ tool: TOOL_NAME,
2471
+ params: {
2472
+ prompt: input.prompt,
2473
+ aspect_ratio: input.aspect_ratio,
2474
+ num_images: input.num_images,
2475
+ rendering_speed: input.rendering_speed,
2476
+ magic_prompt: input.magic_prompt,
2477
+ style_type: input.style_type,
2478
+ save_locally: input.save_locally
2479
+ }
2480
+ });
2481
+ try {
2482
+ const generateParams = {
2483
+ prompt: input.prompt
2484
+ };
2485
+ if (input.negative_prompt !== void 0) {
2486
+ generateParams.negativePrompt = input.negative_prompt;
2487
+ }
2488
+ if (input.aspect_ratio !== void 0) {
2489
+ generateParams.aspectRatio = input.aspect_ratio;
2490
+ }
2491
+ if (input.num_images !== void 0) {
2492
+ generateParams.numImages = input.num_images;
2493
+ }
2494
+ if (input.seed !== void 0) {
2495
+ generateParams.seed = input.seed;
2496
+ }
2497
+ if (input.rendering_speed !== void 0) {
2498
+ generateParams.renderingSpeed = input.rendering_speed;
2499
+ }
2500
+ if (input.magic_prompt !== void 0) {
2501
+ generateParams.magicPrompt = input.magic_prompt;
2502
+ }
2503
+ if (input.style_type !== void 0) {
2504
+ generateParams.styleType = input.style_type;
2505
+ }
2506
+ const response = await client.generate(generateParams);
2507
+ const cost = calculateCost({
2508
+ numImages: response.data.length,
2509
+ renderingSpeed: input.rendering_speed
2510
+ });
2511
+ const images = [];
2512
+ const shouldSaveLocally = input.save_locally && storage.isEnabled();
2513
+ if (shouldSaveLocally) {
2514
+ const urls = response.data.map((img) => img.url);
2515
+ const saveResult = await storage.downloadImages(urls, {
2516
+ prefix: "generated"
2517
+ });
2518
+ for (let i = 0; i < response.data.length; i++) {
2519
+ const apiImage = response.data[i];
2520
+ if (!apiImage)
2521
+ continue;
2522
+ const savedImage = saveResult.saved.find(
2523
+ (s) => s.originalUrl === apiImage.url
2524
+ );
2525
+ const outputImage = {
2526
+ url: apiImage.url,
2527
+ seed: apiImage.seed,
2528
+ is_image_safe: apiImage.is_image_safe
2529
+ };
2530
+ if (savedImage) {
2531
+ outputImage.local_path = savedImage.filePath;
2532
+ }
2533
+ if (apiImage.prompt !== void 0) {
2534
+ outputImage.prompt = apiImage.prompt;
2535
+ }
2536
+ if (apiImage.resolution !== void 0) {
2537
+ outputImage.resolution = apiImage.resolution;
2538
+ }
2539
+ images.push(outputImage);
2540
+ }
2541
+ if (saveResult.failureCount > 0) {
2542
+ log.warn(
2543
+ { failedCount: saveResult.failureCount },
2544
+ "Some images failed to save locally"
2545
+ );
2546
+ }
2547
+ } else {
2548
+ for (const apiImage of response.data) {
2549
+ const outputImage = {
2550
+ url: apiImage.url,
2551
+ seed: apiImage.seed,
2552
+ is_image_safe: apiImage.is_image_safe
2553
+ };
2554
+ if (apiImage.prompt !== void 0) {
2555
+ outputImage.prompt = apiImage.prompt;
2556
+ }
2557
+ if (apiImage.resolution !== void 0) {
2558
+ outputImage.resolution = apiImage.resolution;
2559
+ }
2560
+ images.push(outputImage);
2561
+ }
2562
+ }
2563
+ const result = {
2564
+ success: true,
2565
+ created: response.created,
2566
+ images,
2567
+ total_cost: toCostEstimateOutput(cost),
2568
+ num_images: images.length
2569
+ };
2570
+ const durationMs = Date.now() - startTime;
2571
+ logToolResult(log, {
2572
+ tool: TOOL_NAME,
2573
+ success: true,
2574
+ durationMs
2575
+ });
2576
+ log.debug(
2577
+ {
2578
+ numImages: result.num_images,
2579
+ creditsUsed: result.total_cost.credits_used,
2580
+ durationMs
2581
+ },
2582
+ "Generation completed successfully"
2583
+ );
2584
+ return result;
2585
+ } catch (error) {
2586
+ const mcpError = error instanceof IdeogramMCPError ? error : wrapError(error);
2587
+ const durationMs = Date.now() - startTime;
2588
+ logError(log, mcpError, "Generation failed", {
2589
+ tool: TOOL_NAME,
2590
+ durationMs
2591
+ });
2592
+ logToolResult(log, {
2593
+ tool: TOOL_NAME,
2594
+ success: false,
2595
+ durationMs,
2596
+ errorCode: mcpError.code
2597
+ });
2598
+ return mcpError.toToolError();
2599
+ }
2600
+ };
2601
+ }
2602
+ function getDefaultHandler() {
2603
+ if (!defaultHandler) {
2604
+ defaultHandler = createGenerateHandler();
2605
+ }
2606
+ return defaultHandler;
2607
+ }
2608
+ async function ideogramGenerate(input) {
2609
+ return getDefaultHandler()(input);
2610
+ }
2611
+ var TOOL_NAME, TOOL_DESCRIPTION, TOOL_SCHEMA, defaultHandler, ideogramGenerateTool;
2612
+ var init_generate = __esm({
2613
+ "src/tools/generate.ts"() {
2614
+ "use strict";
2615
+ init_tool_types();
2616
+ init_ideogram_client();
2617
+ init_cost_calculator();
2618
+ init_storage_service();
2619
+ init_error_handler();
2620
+ init_logger();
2621
+ TOOL_NAME = "ideogram_generate";
2622
+ TOOL_DESCRIPTION = `Generate images from text prompts using Ideogram AI v3.
2623
+
2624
+ Creates high-quality AI-generated images based on text descriptions. Supports various aspect ratios, rendering quality levels, and style options.
2625
+
2626
+ Features:
2627
+ - 15 aspect ratio options (1x1, 16x9, 9x16, 4x3, 3x4, 3x2, 2x3, 4x5, 5x4, 1x2, 2x1, 1x3, 3x1, 10x16, 16x10)
2628
+ - Rendering speed options: FLASH (fastest), TURBO (fast), DEFAULT (balanced), QUALITY (best quality)
2629
+ - Magic prompt enhancement to automatically improve prompts
2630
+ - Style types: AUTO, GENERAL, REALISTIC, DESIGN, FICTION
2631
+ - Generate 1-8 images per request
2632
+ - Optional local saving of generated images
2633
+ - Cost tracking for usage monitoring
2634
+
2635
+ Returns image URLs, seeds for reproducibility, and cost estimates.`;
2636
+ TOOL_SCHEMA = GenerateInputSchema;
2637
+ defaultHandler = null;
2638
+ ideogramGenerateTool = {
2639
+ name: TOOL_NAME,
2640
+ description: TOOL_DESCRIPTION,
2641
+ schema: TOOL_SCHEMA,
2642
+ handler: ideogramGenerate
2643
+ };
2644
+ }
2645
+ });
2646
+
2647
+ // src/tools/edit.ts
2648
+ function createEditHandler(options = {}) {
2649
+ const log = options.logger ?? createChildLogger("tool:edit");
2650
+ const client = options.client ?? createIdeogramClient(options.clientOptions);
2651
+ const storage = options.storage ?? createStorageService(options.storageOptions);
2652
+ return async function ideogramEditHandler(input) {
2653
+ const startTime = Date.now();
2654
+ logToolInvocation(log, {
2655
+ tool: TOOL_NAME2,
2656
+ params: {
2657
+ prompt: input.prompt,
2658
+ hasImage: !!input.image,
2659
+ hasMask: !!input.mask,
2660
+ model: input.model,
2661
+ num_images: input.num_images,
2662
+ magic_prompt: input.magic_prompt,
2663
+ style_type: input.style_type,
2664
+ save_locally: input.save_locally
2665
+ }
2666
+ });
2667
+ try {
2668
+ const editParams = {
2669
+ prompt: input.prompt,
2670
+ image: input.image,
2671
+ mask: input.mask
2672
+ };
2673
+ if (input.model !== void 0) {
2674
+ editParams.model = input.model;
2675
+ }
2676
+ if (input.num_images !== void 0) {
2677
+ editParams.numImages = input.num_images;
2678
+ }
2679
+ if (input.seed !== void 0) {
2680
+ editParams.seed = input.seed;
2681
+ }
2682
+ if (input.magic_prompt !== void 0) {
2683
+ editParams.magicPrompt = input.magic_prompt;
2684
+ }
2685
+ if (input.style_type !== void 0) {
2686
+ editParams.styleType = input.style_type;
2687
+ }
2688
+ const response = await client.edit(editParams);
2689
+ const cost = calculateEditCost({
2690
+ numImages: response.data.length,
2691
+ renderingSpeed: "DEFAULT"
2692
+ });
2693
+ const images = [];
2694
+ const shouldSaveLocally = input.save_locally && storage.isEnabled();
2695
+ if (shouldSaveLocally) {
2696
+ const urls = response.data.map((img) => img.url);
2697
+ const saveResult = await storage.downloadImages(urls, {
2698
+ prefix: "edited"
2699
+ });
2700
+ for (let i = 0; i < response.data.length; i++) {
2701
+ const apiImage = response.data[i];
2702
+ if (!apiImage)
2703
+ continue;
2704
+ const savedImage = saveResult.saved.find(
2705
+ (s) => s.originalUrl === apiImage.url
2706
+ );
2707
+ const outputImage = {
2708
+ url: apiImage.url,
2709
+ seed: apiImage.seed,
2710
+ is_image_safe: apiImage.is_image_safe
2711
+ };
2712
+ if (savedImage) {
2713
+ outputImage.local_path = savedImage.filePath;
2714
+ }
2715
+ if (apiImage.prompt !== void 0) {
2716
+ outputImage.prompt = apiImage.prompt;
2717
+ }
2718
+ if (apiImage.resolution !== void 0) {
2719
+ outputImage.resolution = apiImage.resolution;
2720
+ }
2721
+ images.push(outputImage);
2722
+ }
2723
+ if (saveResult.failureCount > 0) {
2724
+ log.warn(
2725
+ { failedCount: saveResult.failureCount },
2726
+ "Some images failed to save locally"
2727
+ );
2728
+ }
2729
+ } else {
2730
+ for (const apiImage of response.data) {
2731
+ const outputImage = {
2732
+ url: apiImage.url,
2733
+ seed: apiImage.seed,
2734
+ is_image_safe: apiImage.is_image_safe
2735
+ };
2736
+ if (apiImage.prompt !== void 0) {
2737
+ outputImage.prompt = apiImage.prompt;
2738
+ }
2739
+ if (apiImage.resolution !== void 0) {
2740
+ outputImage.resolution = apiImage.resolution;
2741
+ }
2742
+ images.push(outputImage);
2743
+ }
2744
+ }
2745
+ const result = {
2746
+ success: true,
2747
+ created: response.created,
2748
+ images,
2749
+ total_cost: toCostEstimateOutput(cost),
2750
+ num_images: images.length
2751
+ };
2752
+ const durationMs = Date.now() - startTime;
2753
+ logToolResult(log, {
2754
+ tool: TOOL_NAME2,
2755
+ success: true,
2756
+ durationMs
2757
+ });
2758
+ log.debug(
2759
+ {
2760
+ numImages: result.num_images,
2761
+ creditsUsed: result.total_cost.credits_used,
2762
+ durationMs
2763
+ },
2764
+ "Inpainting completed successfully"
2765
+ );
2766
+ return result;
2767
+ } catch (error) {
2768
+ const mcpError = error instanceof IdeogramMCPError ? error : wrapError(error);
2769
+ const durationMs = Date.now() - startTime;
2770
+ logError(log, mcpError, "Edit failed", {
2771
+ tool: TOOL_NAME2,
2772
+ durationMs
2773
+ });
2774
+ logToolResult(log, {
2775
+ tool: TOOL_NAME2,
2776
+ success: false,
2777
+ durationMs,
2778
+ errorCode: mcpError.code
2779
+ });
2780
+ return mcpError.toToolError();
2781
+ }
2782
+ };
2783
+ }
2784
+ function getDefaultHandler2() {
2785
+ if (!defaultHandler2) {
2786
+ defaultHandler2 = createEditHandler();
2787
+ }
2788
+ return defaultHandler2;
2789
+ }
2790
+ async function ideogramEdit(input) {
2791
+ return getDefaultHandler2()(input);
2792
+ }
2793
+ var TOOL_NAME2, TOOL_DESCRIPTION2, TOOL_SCHEMA2, defaultHandler2, ideogramEditTool;
2794
+ var init_edit = __esm({
2795
+ "src/tools/edit.ts"() {
2796
+ "use strict";
2797
+ init_tool_types();
2798
+ init_ideogram_client();
2799
+ init_cost_calculator();
2800
+ init_storage_service();
2801
+ init_error_handler();
2802
+ init_logger();
2803
+ TOOL_NAME2 = "ideogram_edit";
2804
+ TOOL_DESCRIPTION2 = `Edit specific parts of an existing image using inpainting with Ideogram AI.
2805
+
2806
+ Inpainting uses a mask to define which areas to modify:
2807
+ - Black pixels in mask = areas to edit/regenerate
2808
+ - White pixels in mask = areas to preserve unchanged
2809
+
2810
+ The mask must be the same dimensions as the source image and contain only black and white pixels.
2811
+
2812
+ Features:
2813
+ - Mask-based selective editing
2814
+ - Magic prompt enhancement to automatically improve prompts
2815
+ - Style types: AUTO, GENERAL, REALISTIC, DESIGN, FICTION, RENDER_3D, ANIME
2816
+ - Model selection: V_2 (default) or V_2_TURBO (faster)
2817
+ - Generate 1-8 variations per edit operation
2818
+ - Optional local saving of edited images
2819
+ - Cost tracking for usage monitoring
2820
+
2821
+ Input image and mask can be provided as URLs, file paths, or base64 data URLs.
2822
+
2823
+ Returns edited image URLs, seeds for reproducibility, and cost estimates.`;
2824
+ TOOL_SCHEMA2 = EditInputSchema;
2825
+ defaultHandler2 = null;
2826
+ ideogramEditTool = {
2827
+ name: TOOL_NAME2,
2828
+ description: TOOL_DESCRIPTION2,
2829
+ schema: TOOL_SCHEMA2,
2830
+ handler: ideogramEdit
2831
+ };
2832
+ }
2833
+ });
2834
+
2835
+ // src/tools/generate-async.ts
2836
+ function createGenerateAsyncHandler(options = {}) {
2837
+ const log = options.logger ?? createChildLogger("tool:generate-async");
2838
+ const store = options.store ?? createPredictionStore(options.storeOptions);
2839
+ return async function ideogramGenerateAsyncHandler(input) {
2840
+ const startTime = Date.now();
2841
+ logToolInvocation(log, {
2842
+ tool: TOOL_NAME3,
2843
+ params: {
2844
+ prompt: input.prompt,
2845
+ aspect_ratio: input.aspect_ratio,
2846
+ num_images: input.num_images,
2847
+ rendering_speed: input.rendering_speed,
2848
+ magic_prompt: input.magic_prompt,
2849
+ style_type: input.style_type,
2850
+ save_locally: input.save_locally,
2851
+ webhook_url: input.webhook_url
2852
+ }
2853
+ });
2854
+ try {
2855
+ const generateRequest = {
2856
+ prompt: input.prompt
2857
+ };
2858
+ if (input.negative_prompt !== void 0) {
2859
+ generateRequest.negative_prompt = input.negative_prompt;
2860
+ }
2861
+ if (input.aspect_ratio !== void 0) {
2862
+ generateRequest.aspect_ratio = input.aspect_ratio;
2863
+ }
2864
+ if (input.num_images !== void 0) {
2865
+ generateRequest.num_images = input.num_images;
2866
+ }
2867
+ if (input.seed !== void 0) {
2868
+ generateRequest.seed = input.seed;
2869
+ }
2870
+ if (input.rendering_speed !== void 0) {
2871
+ generateRequest.rendering_speed = input.rendering_speed;
2872
+ }
2873
+ if (input.magic_prompt !== void 0) {
2874
+ generateRequest.magic_prompt = input.magic_prompt;
2875
+ }
2876
+ if (input.style_type !== void 0) {
2877
+ generateRequest.style_type = input.style_type;
2878
+ }
2879
+ const createOptions = {
2880
+ request: generateRequest,
2881
+ type: "generate"
2882
+ };
2883
+ if (input.webhook_url !== void 0) {
2884
+ createOptions.webhookUrl = input.webhook_url;
2885
+ }
2886
+ const prediction = store.create(createOptions);
2887
+ const result = {
2888
+ success: true,
2889
+ prediction_id: prediction.id,
2890
+ status: "queued",
2891
+ eta_seconds: prediction.eta_seconds ?? 30,
2892
+ message: `Image generation queued successfully. Use ideogram_get_prediction with prediction_id "${prediction.id}" to check status and retrieve results.`
2893
+ };
2894
+ const durationMs = Date.now() - startTime;
2895
+ logToolResult(log, {
2896
+ tool: TOOL_NAME3,
2897
+ success: true,
2898
+ durationMs
2899
+ });
2900
+ log.debug(
2901
+ {
2902
+ predictionId: prediction.id,
2903
+ etaSeconds: result.eta_seconds,
2904
+ durationMs
2905
+ },
2906
+ "Async generation queued successfully"
2907
+ );
2908
+ return result;
2909
+ } catch (error) {
2910
+ const mcpError = error instanceof IdeogramMCPError ? error : wrapError(error);
2911
+ const durationMs = Date.now() - startTime;
2912
+ logError(log, mcpError, "Async generation queueing failed", {
2913
+ tool: TOOL_NAME3,
2914
+ durationMs
2915
+ });
2916
+ logToolResult(log, {
2917
+ tool: TOOL_NAME3,
2918
+ success: false,
2919
+ durationMs,
2920
+ errorCode: mcpError.code
2921
+ });
2922
+ return mcpError.toToolError();
2923
+ }
2924
+ };
2925
+ }
2926
+ function getDefaultStore() {
2927
+ if (!defaultStore) {
2928
+ defaultStore = createPredictionStore();
2929
+ }
2930
+ return defaultStore;
2931
+ }
2932
+ function getDefaultHandler3() {
2933
+ if (!defaultHandler3) {
2934
+ defaultHandler3 = createGenerateAsyncHandler({
2935
+ store: getDefaultStore()
2936
+ });
2937
+ }
2938
+ return defaultHandler3;
2939
+ }
2940
+ async function ideogramGenerateAsync(input) {
2941
+ return getDefaultHandler3()(input);
2942
+ }
2943
+ var TOOL_NAME3, TOOL_DESCRIPTION3, TOOL_SCHEMA3, defaultHandler3, defaultStore, ideogramGenerateAsyncTool;
2944
+ var init_generate_async = __esm({
2945
+ "src/tools/generate-async.ts"() {
2946
+ "use strict";
2947
+ init_tool_types();
2948
+ init_prediction_store();
2949
+ init_error_handler();
2950
+ init_logger();
2951
+ TOOL_NAME3 = "ideogram_generate_async";
2952
+ TOOL_DESCRIPTION3 = `Queue an image generation request for background processing.
2953
+
2954
+ Returns immediately with a prediction_id that can be used to poll for status and results using ideogram_get_prediction.
2955
+
2956
+ This is a LOCAL async implementation since the Ideogram API is synchronous only. Jobs are queued internally and processed in order. Use this when you want to:
2957
+ - Queue multiple generations without waiting
2958
+ - Continue working while images generate in the background
2959
+ - Have more control over the generation workflow
2960
+
2961
+ Parameters are the same as ideogram_generate:
2962
+ - prompt: Text description of the desired image (required)
2963
+ - aspect_ratio: Image dimensions (1x1, 16x9, etc.)
2964
+ - num_images: Number of images to generate (1-8)
2965
+ - rendering_speed: FLASH (fastest), TURBO, DEFAULT, or QUALITY (best)
2966
+ - magic_prompt: AUTO, ON, or OFF for prompt enhancement
2967
+ - style_type: AUTO, GENERAL, REALISTIC, DESIGN, or FICTION
2968
+
2969
+ Returns:
2970
+ - prediction_id: Unique ID for polling
2971
+ - status: 'queued'
2972
+ - eta_seconds: Estimated time to completion
2973
+ - message: Status message
2974
+
2975
+ After calling this, use ideogram_get_prediction to check status and retrieve results.`;
2976
+ TOOL_SCHEMA3 = GenerateAsyncInputSchema;
2977
+ defaultHandler3 = null;
2978
+ defaultStore = null;
2979
+ ideogramGenerateAsyncTool = {
2980
+ name: TOOL_NAME3,
2981
+ description: TOOL_DESCRIPTION3,
2982
+ schema: TOOL_SCHEMA3,
2983
+ handler: ideogramGenerateAsync
2984
+ };
2985
+ }
2986
+ });
2987
+
2988
+ // src/tools/get-prediction.ts
2989
+ var get_prediction_exports = {};
2990
+ __export(get_prediction_exports, {
2991
+ TOOL_DESCRIPTION: () => TOOL_DESCRIPTION4,
2992
+ TOOL_NAME: () => TOOL_NAME4,
2993
+ TOOL_SCHEMA: () => TOOL_SCHEMA4,
2994
+ createGetPredictionHandler: () => createGetPredictionHandler,
2995
+ getDefaultHandler: () => getDefaultHandler4,
2996
+ getDefaultStore: () => getDefaultStore2,
2997
+ ideogramGetPrediction: () => ideogramGetPrediction,
2998
+ ideogramGetPredictionTool: () => ideogramGetPredictionTool,
2999
+ resetDefaultHandler: () => resetDefaultHandler,
3000
+ setDefaultStore: () => setDefaultStore
3001
+ });
3002
+ function createGetPredictionHandler(options = {}) {
3003
+ const log = options.logger ?? createChildLogger("tool:get-prediction");
3004
+ const store = options.store ?? createPredictionStore(options.storeOptions);
3005
+ return async function ideogramGetPredictionHandler(input) {
3006
+ const startTime = Date.now();
3007
+ logToolInvocation(log, {
3008
+ tool: TOOL_NAME4,
3009
+ params: {
3010
+ prediction_id: input.prediction_id
3011
+ }
3012
+ });
3013
+ try {
3014
+ const prediction = store.getOrThrow(input.prediction_id);
3015
+ let result;
3016
+ switch (prediction.status) {
3017
+ case "queued":
3018
+ case "processing": {
3019
+ const processingResult = {
3020
+ success: true,
3021
+ prediction_id: prediction.id,
3022
+ status: prediction.status,
3023
+ message: `Prediction is ${formatPredictionStatus(prediction.status).toLowerCase()}. Please poll again in a few seconds.`
3024
+ };
3025
+ if (prediction.eta_seconds !== void 0) {
3026
+ processingResult.eta_seconds = prediction.eta_seconds;
3027
+ }
3028
+ if (prediction.progress !== void 0) {
3029
+ processingResult.progress = prediction.progress;
3030
+ }
3031
+ result = processingResult;
3032
+ break;
3033
+ }
3034
+ case "completed": {
3035
+ const images = [];
3036
+ const apiResult = prediction.result;
3037
+ if (apiResult?.data) {
3038
+ for (const apiImage of apiResult.data) {
3039
+ const outputImage = {
3040
+ url: apiImage.url,
3041
+ seed: apiImage.seed,
3042
+ is_image_safe: apiImage.is_image_safe
3043
+ };
3044
+ if (apiImage.prompt !== void 0) {
3045
+ outputImage.prompt = apiImage.prompt;
3046
+ }
3047
+ if (apiImage.resolution !== void 0) {
3048
+ outputImage.resolution = apiImage.resolution;
3049
+ }
3050
+ images.push(outputImage);
3051
+ }
3052
+ }
3053
+ const renderingSpeed = prediction.request.rendering_speed ?? "DEFAULT";
3054
+ const numImages = images.length || prediction.request.num_images || 1;
3055
+ const cost = prediction.type === "edit" ? calculateEditCost({ numImages, renderingSpeed }) : calculateCost({ numImages, renderingSpeed });
3056
+ const completedResult = {
3057
+ success: true,
3058
+ prediction_id: prediction.id,
3059
+ status: "completed",
3060
+ created: prediction.created_at,
3061
+ images,
3062
+ total_cost: toCostEstimateOutput(cost),
3063
+ num_images: images.length
3064
+ };
3065
+ result = completedResult;
3066
+ break;
3067
+ }
3068
+ case "failed":
3069
+ case "cancelled": {
3070
+ const failedResult = {
3071
+ success: false,
3072
+ prediction_id: prediction.id,
3073
+ status: prediction.status,
3074
+ error: prediction.error ?? {
3075
+ code: prediction.status === "cancelled" ? "CANCELLED" : "UNKNOWN_ERROR",
3076
+ message: prediction.status === "cancelled" ? "Prediction was cancelled by user" : "Prediction failed with an unknown error",
3077
+ retryable: false
3078
+ },
3079
+ message: prediction.status === "cancelled" ? "This prediction was cancelled. Create a new async generation request to try again." : `Prediction failed: ${prediction.error?.message ?? "Unknown error"}. ${prediction.error?.retryable ? "This error may be retryable." : "Please check your input and try again."}`
3080
+ };
3081
+ result = failedResult;
3082
+ break;
3083
+ }
3084
+ default: {
3085
+ const unknownResult = {
3086
+ success: false,
3087
+ prediction_id: prediction.id,
3088
+ status: "failed",
3089
+ error: {
3090
+ code: "UNKNOWN_STATUS",
3091
+ message: `Unknown prediction status: ${prediction.status}`,
3092
+ retryable: false
3093
+ },
3094
+ message: "Prediction has an unknown status. Please contact support."
3095
+ };
3096
+ result = unknownResult;
3097
+ }
3098
+ }
3099
+ const durationMs = Date.now() - startTime;
3100
+ logToolResult(log, {
3101
+ tool: TOOL_NAME4,
3102
+ success: true,
3103
+ durationMs
3104
+ });
3105
+ log.debug(
3106
+ {
3107
+ predictionId: prediction.id,
3108
+ status: prediction.status,
3109
+ durationMs
3110
+ },
3111
+ "Prediction status retrieved successfully"
3112
+ );
3113
+ return result;
3114
+ } catch (error) {
3115
+ const mcpError = error instanceof IdeogramMCPError ? error : wrapError(error);
3116
+ const durationMs = Date.now() - startTime;
3117
+ logError(log, mcpError, "Get prediction failed", {
3118
+ tool: TOOL_NAME4,
3119
+ predictionId: input.prediction_id,
3120
+ durationMs
3121
+ });
3122
+ logToolResult(log, {
3123
+ tool: TOOL_NAME4,
3124
+ success: false,
3125
+ durationMs,
3126
+ errorCode: mcpError.code
3127
+ });
3128
+ return mcpError.toToolError();
3129
+ }
3130
+ };
3131
+ }
3132
+ function setDefaultStore(store) {
3133
+ defaultStore2 = store;
3134
+ defaultHandler4 = null;
3135
+ }
3136
+ function getDefaultStore2() {
3137
+ if (!defaultStore2) {
3138
+ defaultStore2 = createPredictionStore();
3139
+ }
3140
+ return defaultStore2;
3141
+ }
3142
+ function getDefaultHandler4() {
3143
+ if (!defaultHandler4) {
3144
+ defaultHandler4 = createGetPredictionHandler({
3145
+ store: getDefaultStore2()
3146
+ });
3147
+ }
3148
+ return defaultHandler4;
3149
+ }
3150
+ function resetDefaultHandler() {
3151
+ if (defaultStore2) {
3152
+ defaultStore2.dispose();
3153
+ defaultStore2 = null;
3154
+ }
3155
+ defaultHandler4 = null;
3156
+ }
3157
+ async function ideogramGetPrediction(input) {
3158
+ return getDefaultHandler4()(input);
3159
+ }
3160
+ var TOOL_NAME4, TOOL_DESCRIPTION4, TOOL_SCHEMA4, defaultHandler4, defaultStore2, ideogramGetPredictionTool;
3161
+ var init_get_prediction = __esm({
3162
+ "src/tools/get-prediction.ts"() {
3163
+ "use strict";
3164
+ init_tool_types();
3165
+ init_prediction_store();
3166
+ init_cost_calculator();
3167
+ init_error_handler();
3168
+ init_logger();
3169
+ TOOL_NAME4 = "ideogram_get_prediction";
3170
+ TOOL_DESCRIPTION4 = `Check the status of an async image generation request.
3171
+
3172
+ This tool polls the local job queue to check the status of a prediction created with ideogram_generate_async. Use it to:
3173
+ - Monitor progress of queued or processing jobs
3174
+ - Retrieve completed results including generated images and cost estimates
3175
+ - Check if a job has failed and get error information
3176
+
3177
+ This is a LOCAL implementation since the Ideogram API is synchronous only. The prediction state is managed by the local job queue.
3178
+
3179
+ Parameters:
3180
+ - prediction_id: The unique ID returned from ideogram_generate_async (required)
3181
+
3182
+ Returns one of:
3183
+ 1. **Processing** (status: 'queued' or 'processing'):
3184
+ - progress: Percentage complete (0-100)
3185
+ - eta_seconds: Estimated time remaining
3186
+ - message: Status description
3187
+
3188
+ 2. **Completed** (status: 'completed'):
3189
+ - images: Array of generated images with URLs, seeds, etc.
3190
+ - total_cost: Estimated credits and USD cost
3191
+ - num_images: Count of generated images
3192
+
3193
+ 3. **Failed** (status: 'failed' or 'cancelled'):
3194
+ - error: { code, message, retryable }
3195
+ - message: Description of what went wrong
3196
+
3197
+ Typical workflow:
3198
+ 1. Call ideogram_generate_async to queue a request
3199
+ 2. Poll with ideogram_get_prediction until status is 'completed' or 'failed'
3200
+ 3. Process the results or handle the error`;
3201
+ TOOL_SCHEMA4 = GetPredictionInputSchema;
3202
+ defaultHandler4 = null;
3203
+ defaultStore2 = null;
3204
+ ideogramGetPredictionTool = {
3205
+ name: TOOL_NAME4,
3206
+ description: TOOL_DESCRIPTION4,
3207
+ schema: TOOL_SCHEMA4,
3208
+ handler: ideogramGetPrediction
3209
+ };
3210
+ }
3211
+ });
3212
+
3213
+ // src/tools/cancel-prediction.ts
3214
+ var cancel_prediction_exports = {};
3215
+ __export(cancel_prediction_exports, {
3216
+ TOOL_DESCRIPTION: () => TOOL_DESCRIPTION5,
3217
+ TOOL_NAME: () => TOOL_NAME5,
3218
+ TOOL_SCHEMA: () => TOOL_SCHEMA5,
3219
+ createCancelPredictionHandler: () => createCancelPredictionHandler,
3220
+ getDefaultHandler: () => getDefaultHandler5,
3221
+ getDefaultStore: () => getDefaultStore3,
3222
+ ideogramCancelPrediction: () => ideogramCancelPrediction,
3223
+ ideogramCancelPredictionTool: () => ideogramCancelPredictionTool,
3224
+ resetDefaultHandler: () => resetDefaultHandler2,
3225
+ setDefaultStore: () => setDefaultStore2
3226
+ });
3227
+ function createCancelPredictionHandler(options = {}) {
3228
+ const log = options.logger ?? createChildLogger("tool:cancel-prediction");
3229
+ const store = options.store ?? createPredictionStore(options.storeOptions);
3230
+ return async function ideogramCancelPredictionHandler(input) {
3231
+ const startTime = Date.now();
3232
+ logToolInvocation(log, {
3233
+ tool: TOOL_NAME5,
3234
+ params: {
3235
+ prediction_id: input.prediction_id
3236
+ }
3237
+ });
3238
+ try {
3239
+ const cancelResult = store.cancel(input.prediction_id);
3240
+ let result;
3241
+ if (cancelResult.success) {
3242
+ const successResult = {
3243
+ success: true,
3244
+ prediction_id: input.prediction_id,
3245
+ status: "cancelled",
3246
+ message: "Prediction successfully cancelled. No credits will be used."
3247
+ };
3248
+ result = successResult;
3249
+ } else {
3250
+ const statusLabels = {
3251
+ queued: "is still queued",
3252
+ processing: "is already being processed by the Ideogram API and cannot be stopped",
3253
+ completed: "has already completed successfully",
3254
+ failed: "has already failed",
3255
+ cancelled: "was already cancelled"
3256
+ };
3257
+ const reasonMessages = {
3258
+ queued: "Prediction is queued but cancellation failed unexpectedly",
3259
+ processing: "Cannot cancel - prediction is already being processed",
3260
+ completed: "Cannot cancel - prediction already completed",
3261
+ failed: "Cannot cancel - prediction already failed",
3262
+ cancelled: "Prediction was already cancelled"
3263
+ };
3264
+ const failedResult = {
3265
+ success: false,
3266
+ prediction_id: input.prediction_id,
3267
+ status: cancelResult.status === "cancelled" ? "failed" : cancelResult.status,
3268
+ reason: reasonMessages[cancelResult.status],
3269
+ message: `Cannot cancel this prediction because it ${statusLabels[cancelResult.status]}. ${cancelResult.status === "processing" ? "The job was already sent to the Ideogram API." : cancelResult.status === "completed" ? "Use ideogram_get_prediction to retrieve the results." : ""}`.trim()
3270
+ };
3271
+ result = failedResult;
3272
+ }
3273
+ const durationMs = Date.now() - startTime;
3274
+ logToolResult(log, {
3275
+ tool: TOOL_NAME5,
3276
+ success: true,
3277
+ durationMs
3278
+ });
3279
+ log.debug(
3280
+ {
3281
+ predictionId: input.prediction_id,
3282
+ cancelled: cancelResult.success,
3283
+ status: cancelResult.status,
3284
+ durationMs
3285
+ },
3286
+ "Cancel prediction request processed"
3287
+ );
3288
+ return result;
3289
+ } catch (error) {
3290
+ const mcpError = error instanceof IdeogramMCPError ? error : wrapError(error);
3291
+ const durationMs = Date.now() - startTime;
3292
+ logError(log, mcpError, "Cancel prediction failed", {
3293
+ tool: TOOL_NAME5,
3294
+ predictionId: input.prediction_id,
3295
+ durationMs
3296
+ });
3297
+ logToolResult(log, {
3298
+ tool: TOOL_NAME5,
3299
+ success: false,
3300
+ durationMs,
3301
+ errorCode: mcpError.code
3302
+ });
3303
+ return mcpError.toToolError();
3304
+ }
3305
+ };
3306
+ }
3307
+ function setDefaultStore2(store) {
3308
+ defaultStore3 = store;
3309
+ defaultHandler5 = null;
3310
+ }
3311
+ function getDefaultStore3() {
3312
+ if (!defaultStore3) {
3313
+ defaultStore3 = createPredictionStore();
3314
+ }
3315
+ return defaultStore3;
3316
+ }
3317
+ function getDefaultHandler5() {
3318
+ if (!defaultHandler5) {
3319
+ defaultHandler5 = createCancelPredictionHandler({
3320
+ store: getDefaultStore3()
3321
+ });
3322
+ }
3323
+ return defaultHandler5;
3324
+ }
3325
+ function resetDefaultHandler2() {
3326
+ if (defaultStore3) {
3327
+ defaultStore3.dispose();
3328
+ defaultStore3 = null;
3329
+ }
3330
+ defaultHandler5 = null;
3331
+ }
3332
+ async function ideogramCancelPrediction(input) {
3333
+ return getDefaultHandler5()(input);
3334
+ }
3335
+ var TOOL_NAME5, TOOL_DESCRIPTION5, TOOL_SCHEMA5, defaultHandler5, defaultStore3, ideogramCancelPredictionTool;
3336
+ var init_cancel_prediction = __esm({
3337
+ "src/tools/cancel-prediction.ts"() {
3338
+ "use strict";
3339
+ init_tool_types();
3340
+ init_prediction_store();
3341
+ init_error_handler();
3342
+ init_logger();
3343
+ TOOL_NAME5 = "ideogram_cancel_prediction";
3344
+ TOOL_DESCRIPTION5 = `Cancel a queued async image generation request.
3345
+
3346
+ This tool cancels a prediction that was created with ideogram_generate_async. It only works for predictions that are still in the queue - once a prediction starts processing (submitted to the Ideogram API), it cannot be cancelled.
3347
+
3348
+ This is a LOCAL implementation since the Ideogram API is synchronous only. Cancellation is managed by the local job queue.
3349
+
3350
+ Parameters:
3351
+ - prediction_id: The unique ID returned from ideogram_generate_async (required)
3352
+
3353
+ Returns one of:
3354
+ 1. **Success** (status: 'cancelled'):
3355
+ - The prediction was successfully cancelled
3356
+ - It will not be processed and no credits will be used
3357
+
3358
+ 2. **Failed** (status: 'processing' | 'completed' | 'failed'):
3359
+ - Cannot cancel because the prediction is already processing or completed
3360
+ - If 'processing': The job was already sent to the Ideogram API
3361
+ - If 'completed': The job finished successfully
3362
+ - If 'failed': The job already failed
3363
+
3364
+ Note: You can only cancel predictions that are in 'queued' status. Once processing begins, the Ideogram API call is in progress and cannot be stopped.
3365
+
3366
+ Typical workflow:
3367
+ 1. Call ideogram_generate_async to queue a request
3368
+ 2. If you change your mind, call ideogram_cancel_prediction before it starts processing
3369
+ 3. Use ideogram_get_prediction to verify the cancellation`;
3370
+ TOOL_SCHEMA5 = CancelPredictionInputSchema;
3371
+ defaultHandler5 = null;
3372
+ defaultStore3 = null;
3373
+ ideogramCancelPredictionTool = {
3374
+ name: TOOL_NAME5,
3375
+ description: TOOL_DESCRIPTION5,
3376
+ schema: TOOL_SCHEMA5,
3377
+ handler: ideogramCancelPrediction
3378
+ };
3379
+ }
3380
+ });
3381
+
3382
+ // src/index.ts
3383
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3384
+
3385
+ // src/server.ts
3386
+ init_constants();
3387
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3388
+
3389
+ // src/tools/index.ts
3390
+ init_prediction_store();
3391
+ init_generate();
3392
+ init_edit();
3393
+ init_generate_async();
3394
+ init_get_prediction();
3395
+ init_cancel_prediction();
3396
+ init_generate();
3397
+ init_edit();
3398
+ init_generate_async();
3399
+ init_get_prediction();
3400
+ init_cancel_prediction();
3401
+ import "zod";
3402
+ var allTools = [
3403
+ ideogramGenerateTool,
3404
+ ideogramEditTool,
3405
+ ideogramGenerateAsyncTool,
3406
+ ideogramGetPredictionTool,
3407
+ ideogramCancelPredictionTool
3408
+ ];
3409
+ var sharedPredictionStore = null;
3410
+ function initializeSharedStore(options) {
3411
+ if (!sharedPredictionStore) {
3412
+ sharedPredictionStore = createPredictionStore(options);
3413
+ Promise.resolve().then(() => (init_get_prediction(), get_prediction_exports)).then(({ setDefaultStore: setDefaultStore3 }) => {
3414
+ if (sharedPredictionStore) {
3415
+ setDefaultStore3(sharedPredictionStore);
3416
+ }
3417
+ });
3418
+ Promise.resolve().then(() => (init_cancel_prediction(), cancel_prediction_exports)).then(({ setDefaultStore: setDefaultStore3 }) => {
3419
+ if (sharedPredictionStore) {
3420
+ setDefaultStore3(sharedPredictionStore);
3421
+ }
3422
+ });
3423
+ }
3424
+ return sharedPredictionStore;
3425
+ }
3426
+ function disposeSharedStore() {
3427
+ if (sharedPredictionStore) {
3428
+ sharedPredictionStore.dispose();
3429
+ sharedPredictionStore = null;
3430
+ }
3431
+ }
3432
+
3433
+ // src/server.ts
3434
+ init_logger();
3435
+ init_constants();
3436
+ var serverLogger = createChildLogger("server");
3437
+ function createServer(options = {}) {
3438
+ const {
3439
+ name = SERVER_INFO.NAME,
3440
+ version = SERVER_INFO.VERSION,
3441
+ toolOptions = {}
3442
+ } = options;
3443
+ serverLogger.info({ name, version }, "Creating MCP server");
3444
+ const server = new McpServer({
3445
+ name,
3446
+ version
3447
+ });
3448
+ const { initializeStore = true, storeOptions } = toolOptions;
3449
+ if (initializeStore) {
3450
+ initializeSharedStore(storeOptions);
3451
+ }
3452
+ serverLogger.debug("Registering tools with server.tool()");
3453
+ for (const tool of allTools) {
3454
+ server.tool(
3455
+ tool.name,
3456
+ tool.description,
3457
+ // Schema is passed as the Zod schema object
3458
+ tool.schema,
3459
+ // Handler wrapper to format response for MCP
3460
+ async (input) => {
3461
+ const handler = tool.handler;
3462
+ const result = await handler(input);
3463
+ return {
3464
+ content: [
3465
+ {
3466
+ type: "text",
3467
+ text: JSON.stringify(result, null, 2)
3468
+ }
3469
+ ]
3470
+ };
3471
+ }
3472
+ );
3473
+ serverLogger.debug({ toolName: tool.name }, "Registered tool");
3474
+ }
3475
+ serverLogger.info(
3476
+ {
3477
+ name,
3478
+ version,
3479
+ toolCount: allTools.length,
3480
+ tools: allTools.map((t) => t.name)
3481
+ },
3482
+ "MCP server created with tools registered"
3483
+ );
3484
+ return server;
3485
+ }
3486
+ async function startServer(transport, options = {}) {
3487
+ const server = createServer(options);
3488
+ serverLogger.info("Connecting transport");
3489
+ try {
3490
+ await server.connect(transport);
3491
+ serverLogger.info("Server connected and ready");
3492
+ } catch (error) {
3493
+ serverLogger.error({ err: error }, "Failed to connect transport");
3494
+ throw error;
3495
+ }
3496
+ const shutdown = async () => {
3497
+ serverLogger.info("Shutting down server");
3498
+ try {
3499
+ disposeSharedStore();
3500
+ await server.close();
3501
+ serverLogger.info("Server shutdown complete");
3502
+ } catch (error) {
3503
+ serverLogger.error({ err: error }, "Error during shutdown");
3504
+ throw error;
3505
+ }
3506
+ };
3507
+ return { server, shutdown };
3508
+ }
3509
+
3510
+ // src/index.ts
3511
+ init_logger();
3512
+ init_config();
3513
+ var entryLogger = createChildLogger("entry");
3514
+ function validateConfiguration() {
3515
+ if (!isConfigValid()) {
3516
+ const errors = getConfigErrors();
3517
+ entryLogger.error(
3518
+ { errors },
3519
+ "Invalid configuration. Please check your environment variables."
3520
+ );
3521
+ process.stderr.write(
3522
+ `Configuration Error: ${errors.join(", ")}
3523
+
3524
+ Required environment variables:
3525
+ IDEOGRAM_API_KEY - Your Ideogram API key (get one at https://ideogram.ai)
3526
+
3527
+ Optional environment variables:
3528
+ LOG_LEVEL - Logging level (debug, info, warn, error). Default: info
3529
+ LOCAL_SAVE_DIR - Directory for saving images. Default: ./ideogram_images
3530
+ ENABLE_LOCAL_SAVE - Enable local image saving. Default: true
3531
+ MAX_CONCURRENT_REQUESTS - Rate limiting. Default: 3
3532
+ REQUEST_TIMEOUT_MS - API timeout in milliseconds. Default: 30000
3533
+ `
3534
+ );
3535
+ process.exit(1);
3536
+ }
3537
+ entryLogger.debug("Configuration validated successfully");
3538
+ }
3539
+ function setupSignalHandlers(shutdown) {
3540
+ process.on("SIGINT", async () => {
3541
+ entryLogger.info("Received SIGINT, shutting down gracefully...");
3542
+ try {
3543
+ await shutdown();
3544
+ process.exit(0);
3545
+ } catch {
3546
+ entryLogger.error("Error during shutdown");
3547
+ process.exit(1);
3548
+ }
3549
+ });
3550
+ process.on("SIGTERM", async () => {
3551
+ entryLogger.info("Received SIGTERM, shutting down gracefully...");
3552
+ try {
3553
+ await shutdown();
3554
+ process.exit(0);
3555
+ } catch {
3556
+ entryLogger.error("Error during shutdown");
3557
+ process.exit(1);
3558
+ }
3559
+ });
3560
+ process.on("uncaughtException", async (error) => {
3561
+ entryLogger.error({ err: error }, "Uncaught exception");
3562
+ try {
3563
+ await shutdown();
3564
+ } finally {
3565
+ process.exit(1);
3566
+ }
3567
+ });
3568
+ process.on("unhandledRejection", async (reason) => {
3569
+ entryLogger.error({ reason }, "Unhandled promise rejection");
3570
+ try {
3571
+ await shutdown();
3572
+ } finally {
3573
+ process.exit(1);
3574
+ }
3575
+ });
3576
+ }
3577
+ async function main() {
3578
+ entryLogger.info(
3579
+ { name: SERVER_INFO.NAME, version: SERVER_INFO.VERSION },
3580
+ "Starting Ideogram MCP Server"
3581
+ );
3582
+ validateConfiguration();
3583
+ const transport = new StdioServerTransport();
3584
+ entryLogger.debug("Created StdioServerTransport");
3585
+ try {
3586
+ const { shutdown } = await startServer(transport);
3587
+ setupSignalHandlers(shutdown);
3588
+ entryLogger.info(
3589
+ {
3590
+ name: SERVER_INFO.NAME,
3591
+ version: SERVER_INFO.VERSION,
3592
+ logLevel: config.logLevel,
3593
+ localSaveEnabled: config.enableLocalSave,
3594
+ localSaveDir: config.localSaveDir
3595
+ },
3596
+ "Ideogram MCP Server started and ready to accept connections"
3597
+ );
3598
+ } catch (error) {
3599
+ entryLogger.error({ err: error }, "Failed to start server");
3600
+ process.exit(1);
3601
+ }
3602
+ }
3603
+ main().catch((error) => {
3604
+ process.stderr.write(`Fatal error: ${error}
3605
+ `);
3606
+ process.exit(1);
3607
+ });