#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import Replicate from "replicate";
import { writeFile } from "fs/promises";
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as http from 'http';
// Check for required environment variable
const REPLICATE_API_TOKEN = process.env.REPLICATE_API_TOKEN;
let replicateConfigured = false;
let replicate: Replicate;
if (!REPLICATE_API_TOKEN) {
console.error('REPLICATE_API_TOKEN environment variable is required');
console.error('Please set your Replicate API token: export REPLICATE_API_TOKEN=r8_your_token_here');
// Server continues running, no process.exit()
} else {
// Configure Replicate client
replicate = new Replicate({
auth: REPLICATE_API_TOKEN,
});
replicateConfigured = true;
}
// Define types based on recraft-ai/recraft-v3 API documentation
interface RecraftImageResult {
url?: string;
urls?: string[];
}
interface RecraftInput {
prompt: string;
size?: string;
aspect_ratio?: string;
style?: string;
}
// Download image function
async function downloadImage(url: string, filename: string): Promise<string> {
return new Promise((resolve, reject) => {
try {
const parsedUrl = new URL(url);
const client = parsedUrl.protocol === 'https:' ? https : http;
// Create images directory if it doesn't exist
const imagesDir = path.join(process.cwd(), 'images');
if (!fs.existsSync(imagesDir)) {
fs.mkdirSync(imagesDir, { recursive: true });
}
const filePath = path.join(imagesDir, filename);
const file = fs.createWriteStream(filePath);
client.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Failed to download image: HTTP ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
resolve(filePath);
});
file.on('error', (err) => {
fs.unlink(filePath, () => {}); // Delete partial file
reject(err);
});
}).on('error', (err) => {
reject(err);
});
} catch (error) {
reject(error);
}
});
}
// Generate safe filename for images
function generateImageFilename(prompt: string, index: number): string {
const safePrompt = prompt
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '_')
.substring(0, 50);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
return `recraft_v3_${safePrompt}_${index}_${timestamp}.webp`;
}
// Create MCP server
const server = new McpServer({
name: "replicate-recraft-v3-server",
version: "1.0.0",
});
// Tool: Generate images with recraft-ai/recraft-v3
server.tool(
"recraft_v3_generate",
{
description: "Generate high-quality images using recraft-ai/recraft-v3 - Advanced image generation model with superior quality and detail via Replicate",
inputSchema: {
type: "object",
properties: {
prompt: {
type: "string",
description: "Text prompt for image generation"
},
size: {
type: "string",
enum: ["1024x1024", "1365x1024", "1024x1365", "1536x1024", "1024x1536", "1820x1024", "1024x1820", "1024x2048", "2048x1024", "1434x1024", "1024x1434", "1024x1280", "1280x1024", "1024x1707", "1707x1024"],
description: "Size of the generated image",
default: "1024x1024"
},
aspect_ratio: {
type: "string",
enum: ["Not set", "1:1", "4:3", "3:4", "3:2", "2:3", "16:9", "9:16", "1:2", "2:1", "7:5", "5:7", "4:5", "5:4", "3:5", "5:3"],
description: "Aspect ratio of the generated image",
default: "Not set"
},
style: {
type: "string",
enum: ["any", "realistic_image", "digital_illustration", "digital_illustration/pixel_art", "digital_illustration/hand_drawn", "digital_illustration/grain", "digital_illustration/infantile_sketch", "digital_illustration/2d_art_poster", "digital_illustration/handmade_3d", "digital_illustration/hand_drawn_outline", "digital_illustration/engraving_color", "digital_illustration/2d_art_poster_2", "realistic_image/b_and_w", "realistic_image/hard_flash", "realistic_image/hdr", "realistic_image/natural_light", "realistic_image/studio_portrait", "realistic_image/enterprise", "realistic_image/motion_blur"],
description: "Style of the generated image",
default: "any"
}
},
required: ["prompt"]
}
},
async (args: any) => {
// Check if Replicate client is configured
if (!replicateConfigured) {
return {
content: [{
type: "text",
text: "Error: REPLICATE_API_TOKEN environment variable is not set. Please configure your Replicate API token."
}],
isError: true
};
}
const {
prompt,
size = "1024x1024",
aspect_ratio = "Not set",
style = "any"
} = args;
try {
// Prepare input for Replicate API
const input: any = {
prompt
};
// Add optional parameters if provided
if (size !== "1024x1024") {
input.size = size;
}
if (aspect_ratio !== "Not set") {
input.aspect_ratio = aspect_ratio;
}
if (style !== "any") {
input.style = style;
}
console.error(`Generating image with recraft-ai/recraft-v3 - prompt: "${prompt}"`);
// Call Replicate recraft-v3 API
const output = await replicate.run("recraft-ai/recraft-v3", { input }) as RecraftImageResult;
// Handle both single URL and array of URLs
const imageUrls: string[] = [];
if (output.url) {
imageUrls.push(output.url);
}
if (output.urls && Array.isArray(output.urls)) {
imageUrls.push(...output.urls);
}
if (imageUrls.length === 0) {
throw new Error("No image URLs returned from the API");
}
// Download images locally
console.error("Downloading images locally...");
const downloadedImages = [];
for (let i = 0; i < imageUrls.length; i++) {
const imageUrl = imageUrls[i];
const filename = generateImageFilename(prompt, i + 1);
try {
const localPath = await downloadImage(imageUrl, filename);
downloadedImages.push({
url: imageUrl,
localPath,
index: i + 1,
filename
});
console.error(`Downloaded: ${filename}`);
} catch (downloadError) {
console.error(`Failed to download image ${i + 1}:`, downloadError);
// Still add the image info without local path
downloadedImages.push({
url: imageUrl,
localPath: null,
index: i + 1,
filename
});
}
}
// Format response with download information
const imageDetails = downloadedImages.map(img => {
let details = `Image ${img.index}:`;
if (img.localPath) {
details += `\n Local Path: ${img.localPath}`;
}
details += `\n Original URL: ${img.url}`;
details += `\n Filename: ${img.filename}`;
return details;
}).join('\n\n');
const responseText = `Successfully generated ${downloadedImages.length} image(s) using recraft-ai/recraft-v3:
Prompt: "${prompt}"
Size: ${size}
Aspect Ratio: ${aspect_ratio}
Style: ${style}
Generated Images:
${imageDetails}
${downloadedImages.some(img => img.localPath) ? 'Images have been downloaded to the local \'images\' directory.' : 'Note: Local download failed, but original URLs are available.'}`;
return {
content: [
{
type: "text",
text: responseText
}
]
};
} catch (error) {
console.error('Error generating image:', error);
let errorMessage = "Failed to generate image with recraft-ai/recraft-v3.";
if (error instanceof Error) {
errorMessage += ` Error: ${error.message}`;
}
return {
content: [
{
type: "text",
text: errorMessage
}
],
isError: true
};
}
}
);
// Tool: Generate images with prediction tracking
server.tool(
"recraft_v3_generate_async",
{
description: "Generate images using recraft-ai/recraft-v3 with prediction tracking for monitoring progress",
inputSchema: {
type: "object",
properties: {
prompt: {
type: "string",
description: "Text prompt for image generation"
},
size: {
type: "string",
enum: ["1024x1024", "1365x1024", "1024x1365", "1536x1024", "1024x1536", "1820x1024", "1024x1820", "1024x2048", "2048x1024", "1434x1024", "1024x1434", "1024x1280", "1280x1024", "1024x1707", "1707x1024"],
description: "Size of the generated image",
default: "1024x1024"
},
aspect_ratio: {
type: "string",
enum: ["Not set", "1:1", "4:3", "3:4", "3:2", "2:3", "16:9", "9:16", "1:2", "2:1", "7:5", "5:7", "4:5", "5:4", "3:5", "5:3"],
description: "Aspect ratio of the generated image",
default: "Not set"
},
style: {
type: "string",
enum: ["any", "realistic_image", "digital_illustration", "digital_illustration/pixel_art", "digital_illustration/hand_drawn", "digital_illustration/grain", "digital_illustration/infantile_sketch", "digital_illustration/2d_art_poster", "digital_illustration/handmade_3d", "digital_illustration/hand_drawn_outline", "digital_illustration/engraving_color", "digital_illustration/2d_art_poster_2", "realistic_image/b_and_w", "realistic_image/hard_flash", "realistic_image/hdr", "realistic_image/natural_light", "realistic_image/studio_portrait", "realistic_image/enterprise", "realistic_image/motion_blur"],
description: "Style of the generated image",
default: "any"
},
webhook: {
type: "string",
description: "Webhook URL to receive updates when the prediction completes"
},
webhook_events_filter: {
type: "array",
items: {
type: "string",
enum: ["start", "output", "logs", "completed"]
},
description: "Events to send to the webhook",
default: ["completed"]
}
},
required: ["prompt"]
}
},
async (args: any) => {
// Check if Replicate client is configured
if (!replicateConfigured) {
return {
content: [{
type: "text",
text: "Error: REPLICATE_API_TOKEN environment variable is not set. Please configure your Replicate API token."
}],
isError: true
};
}
const {
prompt,
size = "1024x1024",
aspect_ratio = "Not set",
style = "any",
webhook,
webhook_events_filter = ["completed"]
} = args;
try {
// Prepare input for Replicate API
const input: any = {
prompt
};
// Add optional parameters if provided
if (size !== "1024x1024") {
input.size = size;
}
if (aspect_ratio !== "Not set") {
input.aspect_ratio = aspect_ratio;
}
if (style !== "any") {
input.style = style;
}
console.error(`Creating prediction for recraft-ai/recraft-v3 - prompt: "${prompt}"`);
// Create prediction with optional webhook
const predictionOptions: any = {
model: "recraft-ai/recraft-v3",
input
};
if (webhook) {
predictionOptions.webhook = webhook;
predictionOptions.webhook_events_filter = webhook_events_filter;
}
const prediction = await replicate.predictions.create(predictionOptions);
const responseText = `Prediction created successfully for recraft-ai/recraft-v3:
Prediction ID: ${prediction.id}
Status: ${prediction.status}
Model: recraft-ai/recraft-v3
Prompt: "${prompt}"
Size: ${size}
Aspect Ratio: ${aspect_ratio}
Style: ${style}
${webhook ? `Webhook: ${webhook}` : 'No webhook configured'}
Use the prediction ID with the 'recraft_v3_get_prediction' tool to check status and get results.`;
return {
content: [
{
type: "text",
text: responseText
}
]
};
} catch (error) {
console.error('Error creating prediction:', error);
let errorMessage = "Failed to create prediction for recraft-ai/recraft-v3.";
if (error instanceof Error) {
errorMessage += ` Error: ${error.message}`;
}
return {
content: [
{
type: "text",
text: errorMessage
}
],
isError: true
};
}
}
);
// Tool: Get prediction status and results
server.tool(
"recraft_v3_get_prediction",
{
description: "Get the status and results of a prediction created with recraft_v3_generate_async",
inputSchema: {
type: "object",
properties: {
prediction_id: {
type: "string",
description: "The prediction ID returned from recraft_v3_generate_async"
}
},
required: ["prediction_id"]
}
},
async (args: any) => {
// Check if Replicate client is configured
if (!replicateConfigured) {
return {
content: [{
type: "text",
text: "Error: REPLICATE_API_TOKEN environment variable is not set. Please configure your Replicate API token."
}],
isError: true
};
}
const { prediction_id } = args;
try {
console.error(`Getting prediction status for: ${prediction_id}`);
const prediction = await replicate.predictions.get(prediction_id);
let responseText = `Prediction Status for ${prediction_id}:
Status: ${prediction.status}
Model: ${prediction.model}
Created: ${prediction.created_at}
${prediction.started_at ? `Started: ${prediction.started_at}` : ''}
${prediction.completed_at ? `Completed: ${prediction.completed_at}` : ''}`;
if (prediction.input) {
const input = prediction.input as RecraftInput;
responseText += `\n\nInput Parameters:`;
responseText += `\nPrompt: "${input.prompt}"`;
if (input.size) responseText += `\nSize: ${input.size}`;
if (input.aspect_ratio && input.aspect_ratio !== "Not set") responseText += `\nAspect Ratio: ${input.aspect_ratio}`;
if (input.style && input.style !== "any") responseText += `\nStyle: ${input.style}`;
}
if (prediction.error) {
responseText += `\n\nError: ${prediction.error}`;
}
if (prediction.logs) {
responseText += `\n\nLogs:\n${prediction.logs}`;
}
if (prediction.output && prediction.status === 'succeeded') {
const output = prediction.output as RecraftImageResult;
const imageUrls: string[] = [];
if (output.url) {
imageUrls.push(output.url);
}
if (output.urls && Array.isArray(output.urls)) {
imageUrls.push(...output.urls);
}
if (imageUrls.length > 0) {
responseText += `\n\nGenerated Images:`;
// Download images locally
const downloadedImages = [];
for (let i = 0; i < imageUrls.length; i++) {
const imageUrl = imageUrls[i];
const input = prediction.input as RecraftInput;
const filename = generateImageFilename(input?.prompt || 'image', i + 1);
try {
const localPath = await downloadImage(imageUrl, filename);
downloadedImages.push({
url: imageUrl,
localPath,
index: i + 1,
filename
});
console.error(`Downloaded: ${filename}`);
} catch (downloadError) {
console.error(`Failed to download image ${i + 1}:`, downloadError);
downloadedImages.push({
url: imageUrl,
localPath: null,
index: i + 1,
filename
});
}
}
downloadedImages.forEach(img => {
responseText += `\n\nImage ${img.index}:`;
if (img.localPath) {
responseText += `\n Local Path: ${img.localPath}`;
}
responseText += `\n Original URL: ${img.url}`;
responseText += `\n Filename: ${img.filename}`;
});
if (downloadedImages.some(img => img.localPath)) {
responseText += `\n\nImages have been downloaded to the local 'images' directory.`;
}
}
}
return {
content: [
{
type: "text",
text: responseText
}
]
};
} catch (error) {
console.error('Error getting prediction:', error);
let errorMessage = "Failed to get prediction status.";
if (error instanceof Error) {
errorMessage += ` Error: ${error.message}`;
}
return {
content: [
{
type: "text",
text: errorMessage
}
],
isError: true
};
}
}
);
// Tool: Cancel a prediction
server.tool(
"recraft_v3_cancel_prediction",
{
description: "Cancel a running prediction to prevent unnecessary work and reduce costs",
inputSchema: {
type: "object",
properties: {
prediction_id: {
type: "string",
description: "The prediction ID to cancel"
}
},
required: ["prediction_id"]
}
},
async (args: any) => {
// Check if Replicate client is configured
if (!replicateConfigured) {
return {
content: [{
type: "text",
text: "Error: REPLICATE_API_TOKEN environment variable is not set. Please configure your Replicate API token."
}],
isError: true
};
}
const { prediction_id } = args;
try {
console.error(`Cancelling prediction: ${prediction_id}`);
const prediction = await replicate.predictions.cancel(prediction_id);
const responseText = `Prediction ${prediction_id} has been cancelled.
Status: ${prediction.status}
Model: ${prediction.model}
Created: ${prediction.created_at}
${prediction.started_at ? `Started: ${prediction.started_at}` : ''}
${prediction.completed_at ? `Completed: ${prediction.completed_at}` : ''}
The prediction has been stopped and will not consume additional resources.`;
return {
content: [
{
type: "text",
text: responseText
}
]
};
} catch (error) {
console.error('Error cancelling prediction:', error);
let errorMessage = "Failed to cancel prediction.";
if (error instanceof Error) {
errorMessage += ` Error: ${error.message}`;
}
return {
content: [
{
type: "text",
text: errorMessage
}
],
isError: true
};
}
}
);
// Handle graceful shutdown
process.on('SIGINT', () => {
console.error('Received SIGINT, shutting down gracefully...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('Received SIGTERM, shutting down gracefully...');
process.exit(0);
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});