@goonnguyen/human-mcp 2.8.3 → 2.9.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/README.md +1 -0
- package/dist/index.js +534 -5
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -165944,7 +165944,7 @@ class GeminiClient {
|
|
|
165944
165944
|
});
|
|
165945
165945
|
}
|
|
165946
165946
|
getImageGenerationModel(modelName) {
|
|
165947
|
-
const imageModelName = modelName || "gemini-2.5-flash-image-preview";
|
|
165947
|
+
const imageModelName = modelName || this.config.gemini.imageModel || "gemini-2.5-flash-image-preview";
|
|
165948
165948
|
return this.genAI.getGenerativeModel({
|
|
165949
165949
|
model: imageModelName,
|
|
165950
165950
|
generationConfig: {
|
|
@@ -167915,6 +167915,263 @@ function estimateVideoSize(duration, aspectRatio) {
|
|
|
167915
167915
|
// src/tools/hands/processors/image-editor.ts
|
|
167916
167916
|
init_logger();
|
|
167917
167917
|
init_image_loader();
|
|
167918
|
+
async function editImage(geminiClient, options, config) {
|
|
167919
|
+
const startTime = Date.now();
|
|
167920
|
+
try {
|
|
167921
|
+
const processedInputImage = await processImageForEditing(options.inputImage);
|
|
167922
|
+
const editingPrompt = buildEditingPrompt(options);
|
|
167923
|
+
logger2.info(`Image editing operation: ${options.operation}`);
|
|
167924
|
+
logger2.info(`Editing prompt: "${editingPrompt}"`);
|
|
167925
|
+
const model = geminiClient.getImageGenerationModel();
|
|
167926
|
+
const requestContent = await buildRequestContent(options, processedInputImage, editingPrompt);
|
|
167927
|
+
const response = await model.generateContent(requestContent);
|
|
167928
|
+
const result = response.response;
|
|
167929
|
+
const candidates = result.candidates;
|
|
167930
|
+
logger2.debug(`Gemini API response structure: ${JSON.stringify({
|
|
167931
|
+
hasCandidates: !!candidates,
|
|
167932
|
+
candidatesLength: candidates?.length,
|
|
167933
|
+
firstCandidate: candidates?.[0] ? {
|
|
167934
|
+
hasContent: !!candidates[0].content,
|
|
167935
|
+
hasParts: !!candidates[0].content?.parts,
|
|
167936
|
+
partsLength: candidates[0].content?.parts?.length
|
|
167937
|
+
} : null
|
|
167938
|
+
})}`);
|
|
167939
|
+
if (!candidates || candidates.length === 0) {
|
|
167940
|
+
logger2.error("No candidates in Gemini response. Full response:", JSON.stringify(result, null, 2));
|
|
167941
|
+
throw new Error("No image candidates returned from Gemini API. This may indicate the API doesn't support image editing yet, or the request format is incorrect.");
|
|
167942
|
+
}
|
|
167943
|
+
const candidate = candidates[0];
|
|
167944
|
+
if (!candidate || !candidate.content) {
|
|
167945
|
+
logger2.error("Invalid candidate structure:", JSON.stringify(candidate, null, 2));
|
|
167946
|
+
throw new Error("Invalid response format from Gemini API: missing candidate content");
|
|
167947
|
+
}
|
|
167948
|
+
if (!candidate.content.parts || candidate.content.parts.length === 0) {
|
|
167949
|
+
logger2.error("No parts in candidate content:", JSON.stringify(candidate.content, null, 2));
|
|
167950
|
+
throw new Error("Invalid response format from Gemini API: missing content parts. Note: Gemini image editing may not be available in the current API version.");
|
|
167951
|
+
}
|
|
167952
|
+
let imageData = null;
|
|
167953
|
+
let mimeType = "image/jpeg";
|
|
167954
|
+
logger2.debug(`Searching for image data in ${candidate.content.parts.length} parts`);
|
|
167955
|
+
for (const part of candidate.content.parts) {
|
|
167956
|
+
logger2.debug(`Part type: ${JSON.stringify(Object.keys(part))}`);
|
|
167957
|
+
if ("inlineData" in part && part.inlineData) {
|
|
167958
|
+
imageData = part.inlineData.data;
|
|
167959
|
+
mimeType = part.inlineData.mimeType || "image/jpeg";
|
|
167960
|
+
logger2.info(`Found image data: ${imageData.length} bytes, type: ${mimeType}`);
|
|
167961
|
+
break;
|
|
167962
|
+
}
|
|
167963
|
+
}
|
|
167964
|
+
if (!imageData) {
|
|
167965
|
+
logger2.error("No image data found in response parts:", JSON.stringify(candidate.content.parts, null, 2));
|
|
167966
|
+
throw new Error("No image data found in Gemini response. The API may have returned text instead of an edited image.");
|
|
167967
|
+
}
|
|
167968
|
+
const processingTime = Date.now() - startTime;
|
|
167969
|
+
let resultData;
|
|
167970
|
+
let format;
|
|
167971
|
+
let filePath;
|
|
167972
|
+
let fileName;
|
|
167973
|
+
let fileUrl;
|
|
167974
|
+
let fileSize;
|
|
167975
|
+
const shouldSaveFile = options.saveToFile !== false;
|
|
167976
|
+
const shouldUploadToR2 = options.uploadToR2 === true;
|
|
167977
|
+
if (shouldSaveFile && config) {
|
|
167978
|
+
try {
|
|
167979
|
+
const savedFile = await saveBase64ToFile(imageData, mimeType, config, {
|
|
167980
|
+
prefix: options.filePrefix || `edited-${options.operation}`,
|
|
167981
|
+
directory: options.saveDirectory,
|
|
167982
|
+
uploadToR2: shouldUploadToR2
|
|
167983
|
+
});
|
|
167984
|
+
filePath = savedFile.filePath;
|
|
167985
|
+
fileName = savedFile.fileName;
|
|
167986
|
+
fileUrl = savedFile.url;
|
|
167987
|
+
fileSize = savedFile.size;
|
|
167988
|
+
logger2.info(`Edited image saved to file: ${filePath}`);
|
|
167989
|
+
if (options.outputFormat === "url") {
|
|
167990
|
+
resultData = fileUrl || filePath || `data:${mimeType};base64,${imageData}`;
|
|
167991
|
+
format = fileUrl ? "url" : "file_path";
|
|
167992
|
+
} else {
|
|
167993
|
+
resultData = `data:${mimeType};base64,${imageData}`;
|
|
167994
|
+
format = "base64_data_uri";
|
|
167995
|
+
}
|
|
167996
|
+
} catch (error) {
|
|
167997
|
+
logger2.warn(`Failed to save edited image file: ${error}. Falling back to base64 only.`);
|
|
167998
|
+
resultData = `data:${mimeType};base64,${imageData}`;
|
|
167999
|
+
format = "base64_data_uri";
|
|
168000
|
+
}
|
|
168001
|
+
} else {
|
|
168002
|
+
if (options.outputFormat === "base64") {
|
|
168003
|
+
resultData = `data:${mimeType};base64,${imageData}`;
|
|
168004
|
+
format = "base64_data_uri";
|
|
168005
|
+
} else {
|
|
168006
|
+
resultData = `data:${mimeType};base64,${imageData}`;
|
|
168007
|
+
format = "base64_data_uri";
|
|
168008
|
+
logger2.warn("URL output format requested but file saving disabled. Returning base64 data URI");
|
|
168009
|
+
}
|
|
168010
|
+
}
|
|
168011
|
+
return {
|
|
168012
|
+
editedImageData: resultData,
|
|
168013
|
+
format,
|
|
168014
|
+
operation: options.operation,
|
|
168015
|
+
processingTime,
|
|
168016
|
+
originalSize: estimateImageSize2(processedInputImage.data),
|
|
168017
|
+
editedSize: estimateImageSize2(imageData),
|
|
168018
|
+
filePath,
|
|
168019
|
+
fileName,
|
|
168020
|
+
fileUrl,
|
|
168021
|
+
fileSize,
|
|
168022
|
+
metadata: {
|
|
168023
|
+
prompt: options.prompt,
|
|
168024
|
+
operation: options.operation,
|
|
168025
|
+
strength: options.strength,
|
|
168026
|
+
guidanceScale: options.guidanceScale,
|
|
168027
|
+
seed: options.seed
|
|
168028
|
+
}
|
|
168029
|
+
};
|
|
168030
|
+
} catch (error) {
|
|
168031
|
+
const processingTime = Date.now() - startTime;
|
|
168032
|
+
logger2.error(`Image editing failed after ${processingTime}ms:`, error);
|
|
168033
|
+
if (error instanceof Error) {
|
|
168034
|
+
if (error.message.includes("API key")) {
|
|
168035
|
+
throw new Error("Invalid or missing Google AI API key. Please check your GOOGLE_GEMINI_API_KEY environment variable.");
|
|
168036
|
+
}
|
|
168037
|
+
if (error.message.includes("quota") || error.message.includes("rate limit")) {
|
|
168038
|
+
throw new Error("API quota exceeded or rate limit reached. Please try again later.");
|
|
168039
|
+
}
|
|
168040
|
+
if (error.message.includes("safety") || error.message.includes("policy")) {
|
|
168041
|
+
throw new Error("Image editing blocked due to safety policies. Please modify your request and try again.");
|
|
168042
|
+
}
|
|
168043
|
+
throw new Error(`Image editing failed: ${error.message}`);
|
|
168044
|
+
}
|
|
168045
|
+
throw new Error("Image editing failed due to an unexpected error");
|
|
168046
|
+
}
|
|
168047
|
+
}
|
|
168048
|
+
async function processImageForEditing(inputImage) {
|
|
168049
|
+
try {
|
|
168050
|
+
const result = await loadImageForProcessing(inputImage, {
|
|
168051
|
+
fetchTimeout: 30000,
|
|
168052
|
+
maxWidth: 1024,
|
|
168053
|
+
maxHeight: 1024,
|
|
168054
|
+
quality: 85
|
|
168055
|
+
});
|
|
168056
|
+
return {
|
|
168057
|
+
data: result.data,
|
|
168058
|
+
mimeType: result.mimeType
|
|
168059
|
+
};
|
|
168060
|
+
} catch (error) {
|
|
168061
|
+
throw new Error(`Failed to process input image: ${error instanceof Error ? error.message : error}`);
|
|
168062
|
+
}
|
|
168063
|
+
}
|
|
168064
|
+
function buildEditingPrompt(options) {
|
|
168065
|
+
let prompt = options.prompt;
|
|
168066
|
+
switch (options.operation) {
|
|
168067
|
+
case "inpaint":
|
|
168068
|
+
if (options.maskPrompt) {
|
|
168069
|
+
prompt = `Using the provided image, ${prompt}. Focus on ${options.maskPrompt}. Keep all other parts of the image unchanged.`;
|
|
168070
|
+
} else {
|
|
168071
|
+
prompt = `Using the provided image, ${prompt}. Ensure the changes blend naturally with the existing image style, lighting, and perspective.`;
|
|
168072
|
+
}
|
|
168073
|
+
break;
|
|
168074
|
+
case "outpaint":
|
|
168075
|
+
const direction = options.expandDirection || "all directions";
|
|
168076
|
+
prompt = `Expand the provided image ${direction === "all" ? "in all directions" : `to the ${direction}`} and add: ${prompt}. Match the original image's style, lighting, and perspective. Seamlessly blend the new content with the existing image.`;
|
|
168077
|
+
if (options.expansionRatio && options.expansionRatio !== 1.5) {
|
|
168078
|
+
prompt += ` Expand by approximately ${Math.round(options.expansionRatio * 100)}%.`;
|
|
168079
|
+
}
|
|
168080
|
+
break;
|
|
168081
|
+
case "style_transfer":
|
|
168082
|
+
prompt = `Transform the provided image to have this style: ${prompt}. Maintain the original composition, objects, and structure while applying the new artistic style.`;
|
|
168083
|
+
if (options.styleStrength) {
|
|
168084
|
+
const strength = options.styleStrength > 0.8 ? "strongly" : options.styleStrength > 0.5 ? "moderately" : "subtly";
|
|
168085
|
+
prompt += ` Apply the style ${strength}.`;
|
|
168086
|
+
}
|
|
168087
|
+
break;
|
|
168088
|
+
case "object_manipulation":
|
|
168089
|
+
if (options.targetObject) {
|
|
168090
|
+
const action = options.manipulationType || "modify";
|
|
168091
|
+
prompt = `In the provided image, ${action} the ${options.targetObject}: ${prompt}`;
|
|
168092
|
+
if (options.targetPosition) {
|
|
168093
|
+
prompt += `. Position: ${options.targetPosition}`;
|
|
168094
|
+
}
|
|
168095
|
+
prompt += `. Keep all other elements unchanged.`;
|
|
168096
|
+
}
|
|
168097
|
+
break;
|
|
168098
|
+
case "multi_image_compose":
|
|
168099
|
+
prompt = `Combine the provided images: ${prompt}`;
|
|
168100
|
+
if (options.compositionLayout) {
|
|
168101
|
+
prompt += `. Use a ${options.compositionLayout} layout`;
|
|
168102
|
+
}
|
|
168103
|
+
prompt += `. Ensure natural blending and consistent lighting across the composition.`;
|
|
168104
|
+
break;
|
|
168105
|
+
}
|
|
168106
|
+
if (options.quality === "high") {
|
|
168107
|
+
prompt += " Generate a high-quality result with fine details and professional finish.";
|
|
168108
|
+
} else if (options.quality === "draft") {
|
|
168109
|
+
prompt += " Provide a quick draft version.";
|
|
168110
|
+
}
|
|
168111
|
+
if (options.negativePrompt) {
|
|
168112
|
+
prompt += ` Do not include: ${options.negativePrompt}.`;
|
|
168113
|
+
}
|
|
168114
|
+
return prompt;
|
|
168115
|
+
}
|
|
168116
|
+
async function buildRequestContent(options, processedInputImage, editingPrompt) {
|
|
168117
|
+
const content = [
|
|
168118
|
+
{
|
|
168119
|
+
inlineData: {
|
|
168120
|
+
data: processedInputImage.data,
|
|
168121
|
+
mimeType: processedInputImage.mimeType
|
|
168122
|
+
}
|
|
168123
|
+
},
|
|
168124
|
+
{
|
|
168125
|
+
text: editingPrompt
|
|
168126
|
+
}
|
|
168127
|
+
];
|
|
168128
|
+
if (options.operation === "style_transfer" && options.styleImage) {
|
|
168129
|
+
try {
|
|
168130
|
+
const processedStyle = await processImageForEditing(options.styleImage);
|
|
168131
|
+
content.push({
|
|
168132
|
+
inlineData: {
|
|
168133
|
+
data: processedStyle.data,
|
|
168134
|
+
mimeType: processedStyle.mimeType
|
|
168135
|
+
}
|
|
168136
|
+
});
|
|
168137
|
+
} catch (error) {
|
|
168138
|
+
logger2.warn(`Failed to process style image: ${error}. Proceeding without style reference.`);
|
|
168139
|
+
}
|
|
168140
|
+
}
|
|
168141
|
+
if (options.operation === "multi_image_compose" && options.secondaryImages) {
|
|
168142
|
+
let imageCount = 1;
|
|
168143
|
+
for (const secondaryImage of options.secondaryImages) {
|
|
168144
|
+
if (imageCount >= 3) {
|
|
168145
|
+
logger2.warn("Gemini supports up to 3 images. Skipping additional images.");
|
|
168146
|
+
break;
|
|
168147
|
+
}
|
|
168148
|
+
try {
|
|
168149
|
+
const processedSecondary = await processImageForEditing(secondaryImage);
|
|
168150
|
+
content.push({
|
|
168151
|
+
inlineData: {
|
|
168152
|
+
data: processedSecondary.data,
|
|
168153
|
+
mimeType: processedSecondary.mimeType
|
|
168154
|
+
}
|
|
168155
|
+
});
|
|
168156
|
+
imageCount++;
|
|
168157
|
+
} catch (error) {
|
|
168158
|
+
logger2.warn(`Failed to process secondary image: ${error}. Skipping this image.`);
|
|
168159
|
+
}
|
|
168160
|
+
}
|
|
168161
|
+
}
|
|
168162
|
+
return content;
|
|
168163
|
+
}
|
|
168164
|
+
function estimateImageSize2(base64Data) {
|
|
168165
|
+
const dataLength = base64Data.length;
|
|
168166
|
+
const estimatedBytes = dataLength * 3 / 4;
|
|
168167
|
+
if (estimatedBytes < 1e5) {
|
|
168168
|
+
return "512x512";
|
|
168169
|
+
} else if (estimatedBytes < 400000) {
|
|
168170
|
+
return "1024x1024";
|
|
168171
|
+
} else {
|
|
168172
|
+
return "1024x1024+";
|
|
168173
|
+
}
|
|
168174
|
+
}
|
|
167918
168175
|
|
|
167919
168176
|
// src/tools/hands/index.ts
|
|
167920
168177
|
init_logger();
|
|
@@ -168070,6 +168327,177 @@ async function registerHandsTool(server, config) {
|
|
|
168070
168327
|
};
|
|
168071
168328
|
}
|
|
168072
168329
|
});
|
|
168330
|
+
server.registerTool("gemini_edit_image", {
|
|
168331
|
+
title: "Gemini Image Editing Tool",
|
|
168332
|
+
description: "Edit images using AI with text-based instructions for inpainting, outpainting, style transfer, object manipulation, and composition. No masks required - just describe what you want to change.",
|
|
168333
|
+
inputSchema: {
|
|
168334
|
+
operation: exports_external.enum([
|
|
168335
|
+
"inpaint",
|
|
168336
|
+
"outpaint",
|
|
168337
|
+
"style_transfer",
|
|
168338
|
+
"object_manipulation",
|
|
168339
|
+
"multi_image_compose"
|
|
168340
|
+
]).describe("Type of image editing operation to perform"),
|
|
168341
|
+
input_image: exports_external.string().describe("Base64 encoded image or file path to the input image"),
|
|
168342
|
+
prompt: exports_external.string().min(1, "Prompt cannot be empty").describe("Text description of the desired edit"),
|
|
168343
|
+
mask_image: exports_external.string().optional().describe("Base64 encoded mask image for inpainting (white = edit area, black = keep)"),
|
|
168344
|
+
mask_prompt: exports_external.string().optional().describe("Text description of the area to mask for editing"),
|
|
168345
|
+
expand_direction: exports_external.enum(["all", "left", "right", "top", "bottom", "horizontal", "vertical"]).optional().describe("Direction to expand the image"),
|
|
168346
|
+
expansion_ratio: exports_external.number().min(0.1).max(3).optional().default(1.5).describe("How much to expand the image (1.0 = no expansion)"),
|
|
168347
|
+
style_image: exports_external.string().optional().describe("Base64 encoded reference image for style transfer"),
|
|
168348
|
+
style_strength: exports_external.number().min(0.1).max(1).optional().default(0.7).describe("Strength of style application"),
|
|
168349
|
+
target_object: exports_external.string().optional().describe("Description of the object to manipulate"),
|
|
168350
|
+
manipulation_type: exports_external.enum(["move", "resize", "remove", "replace", "duplicate"]).optional().describe("Type of object manipulation"),
|
|
168351
|
+
target_position: exports_external.string().optional().describe("New position for the object (e.g., 'center', 'top-left')"),
|
|
168352
|
+
secondary_images: exports_external.array(exports_external.string()).optional().describe("Array of base64 encoded images for composition"),
|
|
168353
|
+
composition_layout: exports_external.enum(["blend", "collage", "overlay", "side_by_side"]).optional().describe("How to combine multiple images"),
|
|
168354
|
+
blend_mode: exports_external.enum(["normal", "multiply", "screen", "overlay", "soft_light"]).optional().describe("Blending mode for image composition"),
|
|
168355
|
+
negative_prompt: exports_external.string().optional().describe("What to avoid in the edited image"),
|
|
168356
|
+
strength: exports_external.number().min(0.1).max(1).optional().default(0.8).describe("Strength of the editing effect"),
|
|
168357
|
+
guidance_scale: exports_external.number().min(1).max(20).optional().default(7.5).describe("How closely to follow the prompt"),
|
|
168358
|
+
seed: exports_external.number().int().min(0).optional().describe("Random seed for reproducible results"),
|
|
168359
|
+
output_format: exports_external.enum(["base64", "url"]).optional().default("base64").describe("Output format for the edited image"),
|
|
168360
|
+
quality: exports_external.enum(["draft", "standard", "high"]).optional().default("standard").describe("Quality level of the editing")
|
|
168361
|
+
}
|
|
168362
|
+
}, async (args) => {
|
|
168363
|
+
try {
|
|
168364
|
+
return await handleImageEditing(geminiClient, args, config);
|
|
168365
|
+
} catch (error) {
|
|
168366
|
+
const mcpError = handleError(error);
|
|
168367
|
+
logger2.error(`Tool gemini_edit_image error:`, mcpError);
|
|
168368
|
+
return {
|
|
168369
|
+
content: [{
|
|
168370
|
+
type: "text",
|
|
168371
|
+
text: `Error: ${mcpError.message}`
|
|
168372
|
+
}],
|
|
168373
|
+
isError: true
|
|
168374
|
+
};
|
|
168375
|
+
}
|
|
168376
|
+
});
|
|
168377
|
+
server.registerTool("gemini_inpaint_image", {
|
|
168378
|
+
title: "Gemini Image Inpainting Tool",
|
|
168379
|
+
description: "Add or modify specific areas of an image using natural language descriptions. No mask required - just describe what to change and where.",
|
|
168380
|
+
inputSchema: {
|
|
168381
|
+
input_image: exports_external.string().describe("Base64 encoded image or file path to the input image"),
|
|
168382
|
+
prompt: exports_external.string().min(1, "Prompt cannot be empty").describe("Text description of what to add or change in the image"),
|
|
168383
|
+
mask_image: exports_external.string().optional().describe("(Optional) Base64 encoded mask image - not used by Gemini but kept for compatibility"),
|
|
168384
|
+
mask_prompt: exports_external.string().optional().describe("Text description of WHERE in the image to make changes (e.g., 'the empty space beside the cat', 'the top-left corner')"),
|
|
168385
|
+
negative_prompt: exports_external.string().optional().describe("What to avoid in the edited area"),
|
|
168386
|
+
strength: exports_external.number().min(0.1).max(1).optional().default(0.8).describe("Strength of the editing effect"),
|
|
168387
|
+
guidance_scale: exports_external.number().min(1).max(20).optional().default(7.5).describe("How closely to follow the prompt"),
|
|
168388
|
+
seed: exports_external.number().int().min(0).optional().describe("Random seed for reproducible results"),
|
|
168389
|
+
output_format: exports_external.enum(["base64", "url"]).optional().default("base64").describe("Output format"),
|
|
168390
|
+
quality: exports_external.enum(["draft", "standard", "high"]).optional().default("standard").describe("Quality level")
|
|
168391
|
+
}
|
|
168392
|
+
}, async (args) => {
|
|
168393
|
+
try {
|
|
168394
|
+
const inpaintArgs = { ...args, operation: "inpaint" };
|
|
168395
|
+
return await handleImageEditing(geminiClient, inpaintArgs, config);
|
|
168396
|
+
} catch (error) {
|
|
168397
|
+
const mcpError = handleError(error);
|
|
168398
|
+
logger2.error(`Tool gemini_inpaint_image error:`, mcpError);
|
|
168399
|
+
return {
|
|
168400
|
+
content: [{
|
|
168401
|
+
type: "text",
|
|
168402
|
+
text: `Error: ${mcpError.message}`
|
|
168403
|
+
}],
|
|
168404
|
+
isError: true
|
|
168405
|
+
};
|
|
168406
|
+
}
|
|
168407
|
+
});
|
|
168408
|
+
server.registerTool("gemini_outpaint_image", {
|
|
168409
|
+
title: "Gemini Image Outpainting Tool",
|
|
168410
|
+
description: "Expand an image beyond its original borders in specified directions",
|
|
168411
|
+
inputSchema: {
|
|
168412
|
+
input_image: exports_external.string().describe("Base64 encoded image or file path to the input image"),
|
|
168413
|
+
prompt: exports_external.string().min(1, "Prompt cannot be empty").describe("Text description of what to add in the expanded areas"),
|
|
168414
|
+
expand_direction: exports_external.enum(["all", "left", "right", "top", "bottom", "horizontal", "vertical"]).optional().default("all").describe("Direction to expand the image"),
|
|
168415
|
+
expansion_ratio: exports_external.number().min(0.1).max(3).optional().default(1.5).describe("How much to expand the image (1.0 = no expansion)"),
|
|
168416
|
+
negative_prompt: exports_external.string().optional().describe("What to avoid in the expanded areas"),
|
|
168417
|
+
strength: exports_external.number().min(0.1).max(1).optional().default(0.8).describe("Strength of the editing effect"),
|
|
168418
|
+
guidance_scale: exports_external.number().min(1).max(20).optional().default(7.5).describe("How closely to follow the prompt"),
|
|
168419
|
+
seed: exports_external.number().int().min(0).optional().describe("Random seed for reproducible results"),
|
|
168420
|
+
output_format: exports_external.enum(["base64", "url"]).optional().default("base64").describe("Output format"),
|
|
168421
|
+
quality: exports_external.enum(["draft", "standard", "high"]).optional().default("standard").describe("Quality level")
|
|
168422
|
+
}
|
|
168423
|
+
}, async (args) => {
|
|
168424
|
+
try {
|
|
168425
|
+
const outpaintArgs = { ...args, operation: "outpaint" };
|
|
168426
|
+
return await handleImageEditing(geminiClient, outpaintArgs, config);
|
|
168427
|
+
} catch (error) {
|
|
168428
|
+
const mcpError = handleError(error);
|
|
168429
|
+
logger2.error(`Tool gemini_outpaint_image error:`, mcpError);
|
|
168430
|
+
return {
|
|
168431
|
+
content: [{
|
|
168432
|
+
type: "text",
|
|
168433
|
+
text: `Error: ${mcpError.message}`
|
|
168434
|
+
}],
|
|
168435
|
+
isError: true
|
|
168436
|
+
};
|
|
168437
|
+
}
|
|
168438
|
+
});
|
|
168439
|
+
server.registerTool("gemini_style_transfer_image", {
|
|
168440
|
+
title: "Gemini Style Transfer Tool",
|
|
168441
|
+
description: "Transfer the style from one image to another using AI",
|
|
168442
|
+
inputSchema: {
|
|
168443
|
+
input_image: exports_external.string().describe("Base64 encoded image or file path to the input image"),
|
|
168444
|
+
prompt: exports_external.string().min(1, "Prompt cannot be empty").describe("Text description of the desired style"),
|
|
168445
|
+
style_image: exports_external.string().optional().describe("Base64 encoded reference image for style transfer"),
|
|
168446
|
+
style_strength: exports_external.number().min(0.1).max(1).optional().default(0.7).describe("Strength of style application"),
|
|
168447
|
+
negative_prompt: exports_external.string().optional().describe("What style elements to avoid"),
|
|
168448
|
+
guidance_scale: exports_external.number().min(1).max(20).optional().default(7.5).describe("How closely to follow the prompt"),
|
|
168449
|
+
seed: exports_external.number().int().min(0).optional().describe("Random seed for reproducible results"),
|
|
168450
|
+
output_format: exports_external.enum(["base64", "url"]).optional().default("base64").describe("Output format"),
|
|
168451
|
+
quality: exports_external.enum(["draft", "standard", "high"]).optional().default("standard").describe("Quality level")
|
|
168452
|
+
}
|
|
168453
|
+
}, async (args) => {
|
|
168454
|
+
try {
|
|
168455
|
+
const styleArgs = { ...args, operation: "style_transfer" };
|
|
168456
|
+
return await handleImageEditing(geminiClient, styleArgs, config);
|
|
168457
|
+
} catch (error) {
|
|
168458
|
+
const mcpError = handleError(error);
|
|
168459
|
+
logger2.error(`Tool gemini_style_transfer_image error:`, mcpError);
|
|
168460
|
+
return {
|
|
168461
|
+
content: [{
|
|
168462
|
+
type: "text",
|
|
168463
|
+
text: `Error: ${mcpError.message}`
|
|
168464
|
+
}],
|
|
168465
|
+
isError: true
|
|
168466
|
+
};
|
|
168467
|
+
}
|
|
168468
|
+
});
|
|
168469
|
+
server.registerTool("gemini_compose_images", {
|
|
168470
|
+
title: "Gemini Image Composition Tool",
|
|
168471
|
+
description: "Combine multiple images into a single composition using AI",
|
|
168472
|
+
inputSchema: {
|
|
168473
|
+
input_image: exports_external.string().describe("Base64 encoded primary image"),
|
|
168474
|
+
secondary_images: exports_external.array(exports_external.string()).describe("Array of base64 encoded secondary images to compose"),
|
|
168475
|
+
prompt: exports_external.string().min(1, "Prompt cannot be empty").describe("Text description of how to compose the images"),
|
|
168476
|
+
composition_layout: exports_external.enum(["blend", "collage", "overlay", "side_by_side"]).optional().default("blend").describe("How to combine the images"),
|
|
168477
|
+
blend_mode: exports_external.enum(["normal", "multiply", "screen", "overlay", "soft_light"]).optional().default("normal").describe("Blending mode for image composition"),
|
|
168478
|
+
negative_prompt: exports_external.string().optional().describe("What to avoid in the composition"),
|
|
168479
|
+
strength: exports_external.number().min(0.1).max(1).optional().default(0.8).describe("Strength of the composition effect"),
|
|
168480
|
+
guidance_scale: exports_external.number().min(1).max(20).optional().default(7.5).describe("How closely to follow the prompt"),
|
|
168481
|
+
seed: exports_external.number().int().min(0).optional().describe("Random seed for reproducible results"),
|
|
168482
|
+
output_format: exports_external.enum(["base64", "url"]).optional().default("base64").describe("Output format"),
|
|
168483
|
+
quality: exports_external.enum(["draft", "standard", "high"]).optional().default("standard").describe("Quality level")
|
|
168484
|
+
}
|
|
168485
|
+
}, async (args) => {
|
|
168486
|
+
try {
|
|
168487
|
+
const composeArgs = { ...args, operation: "multi_image_compose" };
|
|
168488
|
+
return await handleImageEditing(geminiClient, composeArgs, config);
|
|
168489
|
+
} catch (error) {
|
|
168490
|
+
const mcpError = handleError(error);
|
|
168491
|
+
logger2.error(`Tool gemini_compose_images error:`, mcpError);
|
|
168492
|
+
return {
|
|
168493
|
+
content: [{
|
|
168494
|
+
type: "text",
|
|
168495
|
+
text: `Error: ${mcpError.message}`
|
|
168496
|
+
}],
|
|
168497
|
+
isError: true
|
|
168498
|
+
};
|
|
168499
|
+
}
|
|
168500
|
+
});
|
|
168073
168501
|
}
|
|
168074
168502
|
async function handleImageGeneration(geminiClient, args, config) {
|
|
168075
168503
|
const input = ImageGenerationInputSchema.parse(args);
|
|
@@ -168236,6 +168664,103 @@ async function handleImageToVideoGeneration(geminiClient, args, config) {
|
|
|
168236
168664
|
isError: false
|
|
168237
168665
|
};
|
|
168238
168666
|
}
|
|
168667
|
+
async function handleImageEditing(geminiClient, args, config) {
|
|
168668
|
+
const input = ImageEditingInputSchema.parse(args);
|
|
168669
|
+
const {
|
|
168670
|
+
operation,
|
|
168671
|
+
input_image,
|
|
168672
|
+
prompt,
|
|
168673
|
+
mask_image,
|
|
168674
|
+
mask_prompt,
|
|
168675
|
+
expand_direction,
|
|
168676
|
+
expansion_ratio,
|
|
168677
|
+
style_image,
|
|
168678
|
+
style_strength,
|
|
168679
|
+
target_object,
|
|
168680
|
+
manipulation_type,
|
|
168681
|
+
target_position,
|
|
168682
|
+
secondary_images,
|
|
168683
|
+
composition_layout,
|
|
168684
|
+
blend_mode,
|
|
168685
|
+
negative_prompt,
|
|
168686
|
+
strength,
|
|
168687
|
+
guidance_scale,
|
|
168688
|
+
seed,
|
|
168689
|
+
output_format,
|
|
168690
|
+
quality
|
|
168691
|
+
} = input;
|
|
168692
|
+
logger2.info(`Editing image with operation: "${operation}" and prompt: "${prompt}"`);
|
|
168693
|
+
const editingOptions = {
|
|
168694
|
+
operation,
|
|
168695
|
+
inputImage: input_image,
|
|
168696
|
+
prompt,
|
|
168697
|
+
maskImage: mask_image,
|
|
168698
|
+
maskPrompt: mask_prompt,
|
|
168699
|
+
expandDirection: expand_direction,
|
|
168700
|
+
expansionRatio: expansion_ratio || 1.5,
|
|
168701
|
+
styleImage: style_image,
|
|
168702
|
+
styleStrength: style_strength || 0.7,
|
|
168703
|
+
targetObject: target_object,
|
|
168704
|
+
manipulationType: manipulation_type,
|
|
168705
|
+
targetPosition: target_position,
|
|
168706
|
+
secondaryImages: secondary_images,
|
|
168707
|
+
compositionLayout: composition_layout,
|
|
168708
|
+
blendMode: blend_mode,
|
|
168709
|
+
negativePrompt: negative_prompt,
|
|
168710
|
+
strength: strength || 0.8,
|
|
168711
|
+
guidanceScale: guidance_scale || 7.5,
|
|
168712
|
+
seed,
|
|
168713
|
+
outputFormat: output_format || "base64",
|
|
168714
|
+
quality: quality || "standard",
|
|
168715
|
+
fetchTimeout: config.server.fetchTimeout,
|
|
168716
|
+
saveToFile: true,
|
|
168717
|
+
uploadToR2: config.cloudflare?.accessKey ? true : false,
|
|
168718
|
+
filePrefix: `edited-${operation}`
|
|
168719
|
+
};
|
|
168720
|
+
const result = await editImage(geminiClient, editingOptions, config);
|
|
168721
|
+
let base64Data;
|
|
168722
|
+
let mimeType;
|
|
168723
|
+
if (result.editedImageData.startsWith("data:")) {
|
|
168724
|
+
const matches = result.editedImageData.match(/data:([^;]+);base64,(.+)/);
|
|
168725
|
+
if (matches && matches[1] && matches[2]) {
|
|
168726
|
+
mimeType = matches[1];
|
|
168727
|
+
base64Data = matches[2];
|
|
168728
|
+
}
|
|
168729
|
+
}
|
|
168730
|
+
const contextText = `✅ Image edited successfully using ${operation} operation
|
|
168731
|
+
|
|
168732
|
+
**Editing Details:**
|
|
168733
|
+
- Operation: ${operation}
|
|
168734
|
+
- Prompt: "${prompt}"
|
|
168735
|
+
- Format: ${result.format}
|
|
168736
|
+
- Original Size: ${result.originalSize}
|
|
168737
|
+
- Edited Size: ${result.editedSize}
|
|
168738
|
+
- Processing Time: ${result.processingTime}ms
|
|
168739
|
+
- Quality: ${quality}
|
|
168740
|
+
- Timestamp: ${new Date().toISOString()}${result.filePath ? `
|
|
168741
|
+
|
|
168742
|
+
**File Information:**
|
|
168743
|
+
- File Path: ${result.filePath}
|
|
168744
|
+
- File Name: ${result.fileName}
|
|
168745
|
+
- File Size: ${result.fileSize} bytes` : ""}${result.fileUrl ? `
|
|
168746
|
+
- Public URL: ${result.fileUrl}` : ""}${result.metadata ? `
|
|
168747
|
+
|
|
168748
|
+
**Operation Metadata:**
|
|
168749
|
+
- Strength: ${result.metadata.strength}
|
|
168750
|
+
- Guidance Scale: ${result.metadata.guidanceScale}
|
|
168751
|
+
- Seed: ${result.metadata.seed || "random"}` : ""}`;
|
|
168752
|
+
const formattedResponse = formatMediaResponse({
|
|
168753
|
+
url: result.fileUrl,
|
|
168754
|
+
filePath: result.filePath,
|
|
168755
|
+
base64: base64Data,
|
|
168756
|
+
mimeType,
|
|
168757
|
+
size: result.fileSize
|
|
168758
|
+
}, config, contextText);
|
|
168759
|
+
return {
|
|
168760
|
+
content: formattedResponse,
|
|
168761
|
+
isError: false
|
|
168762
|
+
};
|
|
168763
|
+
}
|
|
168239
168764
|
|
|
168240
168765
|
// src/tools/mouth/schemas.ts
|
|
168241
168766
|
var VoiceNames = [
|
|
@@ -171156,7 +171681,8 @@ init_logger();
|
|
|
171156
171681
|
var ConfigSchema = exports_external.object({
|
|
171157
171682
|
gemini: exports_external.object({
|
|
171158
171683
|
apiKey: exports_external.string().min(1, "Google Gemini API key is required"),
|
|
171159
|
-
model: exports_external.string().default("gemini-2.5-flash")
|
|
171684
|
+
model: exports_external.string().default("gemini-2.5-flash"),
|
|
171685
|
+
imageModel: exports_external.string().default("gemini-2.5-flash-image-preview")
|
|
171160
171686
|
}),
|
|
171161
171687
|
transport: exports_external.object({
|
|
171162
171688
|
type: exports_external.enum(["stdio", "http", "both"]).default("stdio"),
|
|
@@ -171245,7 +171771,8 @@ function loadConfig() {
|
|
|
171245
171771
|
return ConfigSchema.parse({
|
|
171246
171772
|
gemini: {
|
|
171247
171773
|
apiKey: process.env.GOOGLE_GEMINI_API_KEY || "",
|
|
171248
|
-
model: process.env.GOOGLE_GEMINI_MODEL || "gemini-2.5-flash"
|
|
171774
|
+
model: process.env.GOOGLE_GEMINI_MODEL || "gemini-2.5-flash",
|
|
171775
|
+
imageModel: process.env.GOOGLE_GEMINI_IMAGE_MODEL || "gemini-2.5-flash-image-preview"
|
|
171249
171776
|
},
|
|
171250
171777
|
transport: {
|
|
171251
171778
|
type: process.env.TRANSPORT_TYPE || "stdio",
|
|
@@ -173151,7 +173678,8 @@ class TransportManager {
|
|
|
173151
173678
|
var ConfigSchema2 = exports_external.object({
|
|
173152
173679
|
gemini: exports_external.object({
|
|
173153
173680
|
apiKey: exports_external.string().min(1, "Google Gemini API key is required"),
|
|
173154
|
-
model: exports_external.string().default("gemini-2.5-flash")
|
|
173681
|
+
model: exports_external.string().default("gemini-2.5-flash"),
|
|
173682
|
+
imageModel: exports_external.string().default("gemini-2.5-flash-image-preview")
|
|
173155
173683
|
}),
|
|
173156
173684
|
transport: exports_external.object({
|
|
173157
173685
|
type: exports_external.enum(["stdio", "http", "both"]).default("stdio"),
|
|
@@ -173240,7 +173768,8 @@ function loadConfig2() {
|
|
|
173240
173768
|
return ConfigSchema2.parse({
|
|
173241
173769
|
gemini: {
|
|
173242
173770
|
apiKey: process.env.GOOGLE_GEMINI_API_KEY || "",
|
|
173243
|
-
model: process.env.GOOGLE_GEMINI_MODEL || "gemini-2.5-flash"
|
|
173771
|
+
model: process.env.GOOGLE_GEMINI_MODEL || "gemini-2.5-flash",
|
|
173772
|
+
imageModel: process.env.GOOGLE_GEMINI_IMAGE_MODEL || "gemini-2.5-flash-image-preview"
|
|
173244
173773
|
},
|
|
173245
173774
|
transport: {
|
|
173246
173775
|
type: process.env.TRANSPORT_TYPE || "stdio",
|