@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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/index.js +3607 -0
- 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
|
+
});
|