@mixio-pro/kalaasetu-mcp 2.1.1-beta → 2.1.3-beta
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/package.json +1 -1
- package/src/index.ts +4 -2
- package/src/tools/generate-image.ts +178 -0
- package/src/utils/logger.ts +3 -9
- package/src/utils/openmeter.ts +2 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "./tools/fal";
|
|
9
9
|
import { createAllFalTools } from "./tools/fal/dynamic-tools";
|
|
10
10
|
import { geminiEditImage, geminiTextToImage } from "./tools/gemini";
|
|
11
|
+
import { generateImageConversational } from "./tools/generate-image";
|
|
11
12
|
import { imageToVideo } from "./tools/image-to-video";
|
|
12
13
|
import { getGenerationStatus } from "./tools/get-status";
|
|
13
14
|
|
|
@@ -27,9 +28,10 @@ async function main() {
|
|
|
27
28
|
await Promise.all([syncFalConfig(), syncPromptEnhancerConfigs()]);
|
|
28
29
|
|
|
29
30
|
// 2. Add Gemini Tools
|
|
30
|
-
server.addTool(geminiTextToImage);
|
|
31
|
-
server.addTool(geminiEditImage);
|
|
31
|
+
// server.addTool(geminiTextToImage);
|
|
32
|
+
// server.addTool(geminiEditImage);
|
|
32
33
|
server.addTool(imageToVideo);
|
|
34
|
+
server.addTool(generateImageConversational);
|
|
33
35
|
|
|
34
36
|
// 3. Add Discovery Tools
|
|
35
37
|
server.addTool(falListPresets);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { GoogleGenAI } from "@google/genai";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { getStorage } from "../storage";
|
|
5
|
+
import { generateTimestampedFilename } from "../utils/filename";
|
|
6
|
+
import { safeToolExecute } from "../utils/tool-wrapper";
|
|
7
|
+
import { ensureLocalFile } from "../utils/url-file";
|
|
8
|
+
import { resolveEnhancer } from "../utils/prompt-enhancer-presets";
|
|
9
|
+
|
|
10
|
+
const ai = new GoogleGenAI({
|
|
11
|
+
apiKey: process.env.GEMINI_API_KEY || "",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
async function fileToGenerativePart(filePath: string) {
|
|
15
|
+
const fileResult = await ensureLocalFile(filePath);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
let imageBytes: Buffer;
|
|
19
|
+
if (fileResult.isTemp) {
|
|
20
|
+
imageBytes = fs.readFileSync(fileResult.path);
|
|
21
|
+
} else {
|
|
22
|
+
const storage = getStorage();
|
|
23
|
+
imageBytes = await storage.readFile(fileResult.path);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
inlineData: {
|
|
28
|
+
data: Buffer.from(imageBytes).toString("base64"),
|
|
29
|
+
mimeType: "image/jpeg",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
} finally {
|
|
33
|
+
fileResult.cleanup();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const generateImageConversational = {
|
|
38
|
+
name: "generateImageConversational",
|
|
39
|
+
description:
|
|
40
|
+
"Generate high-quality images from text prompts using Google's Imagen 3 model via Gemini. " +
|
|
41
|
+
"This tool supports conversational history with reference images and their descriptions. " +
|
|
42
|
+
"Use this tool when you need to provide specific reference images with context/descriptions to guide the generation.",
|
|
43
|
+
parameters: z.object({
|
|
44
|
+
prompt: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe("Detailed text description of the image to generate."),
|
|
47
|
+
aspect_ratio: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(
|
|
51
|
+
"Supported ratios: 1:1, 3:4, 4:3, 9:16, or 16:9. Default is 9:16.",
|
|
52
|
+
),
|
|
53
|
+
output_path: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe(
|
|
57
|
+
"Optional: specific local path or filename to save the image. " +
|
|
58
|
+
"If omitted, a timestamped filename is generated automatically.",
|
|
59
|
+
),
|
|
60
|
+
reference_images: z
|
|
61
|
+
.array(
|
|
62
|
+
z.object({
|
|
63
|
+
path: z
|
|
64
|
+
.string()
|
|
65
|
+
.describe("Local path or URL to the reference image."),
|
|
66
|
+
description: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Optional description of the reference image."),
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
.optional()
|
|
73
|
+
.describe(
|
|
74
|
+
"Optional: List of reference images with optional descriptions to guide the style or content.",
|
|
75
|
+
),
|
|
76
|
+
enhancer_preset: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe(
|
|
80
|
+
"Optional: Name of a prompt enhancer preset to apply (e.g., 'cinematic', 'photorealistic', 'anime'). " +
|
|
81
|
+
"Automatically enhances the prompt with professional style modifiers.",
|
|
82
|
+
),
|
|
83
|
+
}),
|
|
84
|
+
timeoutMs: 300000,
|
|
85
|
+
execute: async (args: {
|
|
86
|
+
prompt: string;
|
|
87
|
+
aspect_ratio?: string;
|
|
88
|
+
output_path?: string;
|
|
89
|
+
reference_images?: { path: string; description?: string }[];
|
|
90
|
+
enhancer_preset?: string;
|
|
91
|
+
}) => {
|
|
92
|
+
return safeToolExecute(
|
|
93
|
+
async () => {
|
|
94
|
+
try {
|
|
95
|
+
// Apply prompt enhancement if preset specified
|
|
96
|
+
let enhancedPrompt = args.prompt;
|
|
97
|
+
if (args.enhancer_preset) {
|
|
98
|
+
const enhancer = resolveEnhancer(args.enhancer_preset);
|
|
99
|
+
if (enhancer.hasTransformations()) {
|
|
100
|
+
enhancedPrompt = enhancer.enhance(args.prompt);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const contents: any[] = [];
|
|
105
|
+
|
|
106
|
+
// Add reference images with descriptions (conversational history style)
|
|
107
|
+
if (args.reference_images && Array.isArray(args.reference_images)) {
|
|
108
|
+
for (const refImg of args.reference_images) {
|
|
109
|
+
if (refImg.description) {
|
|
110
|
+
contents.push({ text: refImg.description });
|
|
111
|
+
}
|
|
112
|
+
const imagePart = await fileToGenerativePart(refImg.path);
|
|
113
|
+
contents.push(imagePart);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add main prompt
|
|
118
|
+
contents.push({ text: enhancedPrompt });
|
|
119
|
+
|
|
120
|
+
const response = await ai.models.generateContent({
|
|
121
|
+
model: "gemini-3-pro-image-preview",
|
|
122
|
+
contents: contents, // Pass contents directly which is interpreted as User role parts
|
|
123
|
+
config: {
|
|
124
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
125
|
+
imageConfig: {
|
|
126
|
+
aspectRatio: args.aspect_ratio || "9:16",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const images = [];
|
|
132
|
+
let textResponse = "";
|
|
133
|
+
|
|
134
|
+
if (response.candidates && response.candidates[0]?.content?.parts) {
|
|
135
|
+
for (const part of response.candidates[0].content.parts) {
|
|
136
|
+
if (part.text) {
|
|
137
|
+
textResponse += part.text;
|
|
138
|
+
} else if (part.inlineData?.data) {
|
|
139
|
+
const imageData = part.inlineData.data;
|
|
140
|
+
// Always save the image
|
|
141
|
+
const outputPath =
|
|
142
|
+
args.output_path ||
|
|
143
|
+
generateTimestampedFilename("generated_image.png");
|
|
144
|
+
const storage = getStorage();
|
|
145
|
+
const url = await storage.writeFile(
|
|
146
|
+
outputPath,
|
|
147
|
+
Buffer.from(imageData, "base64"),
|
|
148
|
+
);
|
|
149
|
+
images.push({
|
|
150
|
+
url,
|
|
151
|
+
filename: outputPath,
|
|
152
|
+
mimeType: "image/png",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (images.length > 0) {
|
|
159
|
+
return JSON.stringify({
|
|
160
|
+
url: images?.[0]?.url,
|
|
161
|
+
images,
|
|
162
|
+
message: textResponse || "Image generated successfully",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
textResponse ||
|
|
168
|
+
"Image generation completed but no image was produced"
|
|
169
|
+
);
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
throw new Error(`Image generation failed: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"gemini-generateImageConversational",
|
|
175
|
+
{ toolName: "generateImageConversational" },
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
};
|
package/src/utils/logger.ts
CHANGED
|
@@ -13,9 +13,8 @@ const DEFAULT_SENTRY_DSN =
|
|
|
13
13
|
"https://349c280f4b1c731bc1ac1a1189cae5e7@o4510770034245632.ingest.us.sentry.io/4510770047025152";
|
|
14
14
|
|
|
15
15
|
// Custom format for console output (cleaner for MCP inspector)
|
|
16
|
-
const consoleFormat = printf(({ level, message, timestamp
|
|
17
|
-
|
|
18
|
-
return `${timestamp} [${level}]${clientId} ${message}`;
|
|
16
|
+
const consoleFormat = printf(({ level, message, timestamp }) => {
|
|
17
|
+
return `${timestamp} [${level}] ${message}`;
|
|
19
18
|
});
|
|
20
19
|
|
|
21
20
|
// Determine log level from environment
|
|
@@ -58,12 +57,7 @@ transports.push(
|
|
|
58
57
|
export const logger = winston.createLogger({
|
|
59
58
|
level: LOG_LEVEL,
|
|
60
59
|
format: combine(timestamp(), winston.format.json()),
|
|
61
|
-
defaultMeta: {
|
|
62
|
-
service: "kalaasetu-mcp",
|
|
63
|
-
tags: {
|
|
64
|
-
client_id: process.env.CLIENT_ID || "unknown",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
60
|
+
defaultMeta: { service: "kalaasetu-mcp" },
|
|
67
61
|
transports,
|
|
68
62
|
});
|
|
69
63
|
|
package/src/utils/openmeter.ts
CHANGED
|
@@ -53,7 +53,7 @@ export function initOpenMeter(): OpenMeter | null {
|
|
|
53
53
|
baseUrl: OPENMETER_CONFIG.baseUrl,
|
|
54
54
|
apiKey: OPENMETER_CONFIG.apiToken,
|
|
55
55
|
});
|
|
56
|
-
logger.info(`[OpenMeter] Client initialized`);
|
|
56
|
+
logger.info(`[OpenMeter] Client initialized for customer: ${clientId}`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
return openmeterClient;
|
|
@@ -114,7 +114,7 @@ export async function trackToolCall(context: ToolCallContext): Promise<void> {
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
logger.info(
|
|
117
|
-
`[OpenMeter] Tracked tool call: ${context.toolName} (${creditConfig.credits} credits)`,
|
|
117
|
+
`[OpenMeter] Tracked tool call: ${context.toolName} (${creditConfig.credits} credits) for customer: ${clientId}`,
|
|
118
118
|
);
|
|
119
119
|
} catch (error) {
|
|
120
120
|
// Log but don't throw - tracking failure shouldn't break tool execution
|