Together AI Image MCP Server
- src
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import Together from 'together-ai';
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
const API_KEY = process.env.TOGETHER_API_KEY;
if (!API_KEY) {
throw new Error('TOGETHER_API_KEY environment variable is required');
}
interface GenerateImageArgs {
prompt: string;
model?: string;
width?: number;
height?: number;
steps?: number;
n?: number;
outputDir?: string;
format?: 'png' | 'jpg' | 'svg';
}
const isValidGenerateImageArgs = (args: any): args is GenerateImageArgs => {
return (
typeof args === 'object' &&
args !== null &&
typeof args.prompt === 'string' &&
(args.model === undefined || typeof args.model === 'string') &&
(args.width === undefined || typeof args.width === 'number') &&
(args.height === undefined || typeof args.height === 'number') &&
(args.steps === undefined || typeof args.steps === 'number') &&
(args.n === undefined || typeof args.n === 'number') &&
(args.outputDir === undefined || typeof args.outputDir === 'string') &&
(args.format === undefined || ['png', 'jpg', 'svg'].includes(args.format))
);
};
class TogetherAIImageServer {
private server: Server;
private together: Together;
constructor() {
this.server = new Server(
{
name: 'togetherai-image-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
this.together = new Together({ apiKey: API_KEY });
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'generate_image',
description: 'Generate an image using Together AI',
uiSchema: {
format: {
"ui:widget": "select",
"ui:options": {
label: "Image Format",
position: "above-chat"
}
}
},
inputSchema: {
type: 'object',
properties: {
prompt: {
type: 'string',
description: 'Text description of the image to generate',
},
model: {
type: 'string',
description: 'Model to use for generation',
default: 'black-forest-labs/FLUX.1.1-pro',
},
width: {
type: 'number',
description: 'Image width in pixels',
default: 1024,
},
height: {
type: 'number',
description: 'Image height in pixels',
default: 768,
},
steps: {
type: 'number',
description: 'Number of inference steps',
default: 28,
},
n: {
type: 'number',
description: 'Number of images to generate',
default: 1,
},
outputDir: {
type: 'string',
description: 'Full absolute path where images will be saved (e.g., /Users/username/Projects/myapp/src/assets)',
pattern: '^/',
examples: ['/Users/asanstefanski/Private Projekte/democline/src/assets'],
},
format: {
type: 'string',
enum: ['png', 'jpg', 'svg'],
description: 'Output format for the generated images',
default: 'png',
},
},
required: ['prompt'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'generate_image') {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
if (!request.params.arguments || !isValidGenerateImageArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid generate_image arguments'
);
}
try {
const args = request.params.arguments as GenerateImageArgs;
// Get requested dimensions
const requestWidth = args.width || 1024;
const requestHeight = args.height || 768;
// Ensure dimensions are at least 256 pixels for the API request
const apiWidth = Math.max(256, requestWidth);
const apiHeight = Math.max(256, requestHeight);
const response = await this.together.images.create({
model: args.model || 'black-forest-labs/FLUX.1.1-pro',
prompt: args.prompt,
width: apiWidth,
height: apiHeight,
steps: args.steps || 28,
n: args.n || 1,
response_format: 'base64',
});
// Use provided output directory or default to 'output'
const outputDir = args.outputDir
? path.resolve(args.outputDir)
: path.join(process.cwd(), 'output');
// Create output directory if it doesn't exist
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Process each generated image
const results = await Promise.all(response.data.map(async (result: any, index: number) => {
const imageData = result.b64_json;
let buffer = Buffer.from(imageData, 'base64');
// Only resize if we need to scale down to match requested dimensions
if (requestWidth < 256 || requestHeight < 256) {
const metadata = await sharp(buffer).metadata();
const originalWidth = metadata.width || 0;
const originalHeight = metadata.height || 0;
// Calculate target dimensions maintaining aspect ratio
const aspectRatio = originalWidth / originalHeight;
let targetWidth = requestWidth;
let targetHeight = requestHeight;
if (requestWidth < 256) {
targetWidth = requestWidth;
targetHeight = Math.round(requestWidth / aspectRatio);
}
if (requestHeight < 256) {
targetHeight = requestHeight;
targetWidth = Math.round(requestHeight * aspectRatio);
}
// Resize to match requested dimensions
buffer = await sharp(buffer)
.resize(targetWidth, targetHeight, {
fit: 'contain',
background: { r: 255, g: 255, b: 255, alpha: 1 }
})
.toBuffer();
}
// Save image with timestamp and index
const timestamp = new Date().getTime();
const format = args.format || 'png';
const filename = `image_${timestamp}_${index}.${format}`;
const filepath = path.join(outputDir, filename);
let sharpInstance = sharp(buffer);
switch (format) {
case 'png':
await sharpInstance.png().toFile(filepath);
break;
case 'jpg':
await sharpInstance.jpeg({ quality: 90 }).toFile(filepath);
break;
case 'svg':
// For SVG, we'll need to trace the bitmap to create a vector
await sharpInstance
.png()
.toFile(filepath.replace('.svg', '.png'));
// Note: Actual SVG conversion would require additional processing
// Consider using potrace or similar library for proper SVG conversion
console.warn('SVG output is not fully supported yet');
break;
}
return {
...result,
filepath,
filename,
dimensions: {
original: { width: apiWidth, height: apiHeight },
final: await sharp(filepath).metadata().then(m => ({
width: m.width,
height: m.height
}))
}
};
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
} catch (error: any) {
console.error('Together AI API error:', error);
throw new McpError(
ErrorCode.InternalError,
`Image generation failed: ${error?.message || 'Unknown error'}`
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Together AI Image MCP server running on stdio');
}
}
const server = new TogetherAIImageServer();
server.run().catch(console.error);