Stability AI MCP Server

import axios, { AxiosInstance } from "axios"; import FormData from "form-data"; import fs from "fs"; interface GenerateImageCoreOptions { aspectRatio?: | "16:9" | "1:1" | "21:9" | "2:3" | "3:2" | "4:5" | "5:4" | "9:16" | "9:21"; negativePrompt?: string; seed?: number; stylePreset?: | "3d-model" | "analog-film" | "anime" | "cinematic" | "comic-book" | "digital-art" | "enhance" | "fantasy-art" | "isometric" | "line-art" | "low-poly" | "modeling-compound" | "neon-punk" | "origami" | "photographic" | "pixel-art" | "tile-texture"; outputFormat?: "png" | "jpeg" | "webp"; } interface OutpaintOptions { left?: number; right?: number; up?: number; down?: number; creativity?: number; prompt?: string; seed?: number; outputFormat?: "png" | "jpeg" | "webp"; } interface SearchAndReplaceOptions { searchPrompt: string; prompt: string; } interface UpscaleCreativeOptions { prompt: string; negativePrompt?: string; seed?: number; outputFormat?: "png" | "jpeg" | "webp"; creativity?: number; } interface ControlSketchOptions { prompt: string; controlStrength?: number; negativePrompt?: string; seed?: number; outputFormat?: "png" | "jpeg" | "webp"; } interface SearchAndRecolorOptions { selectPrompt: string; prompt: string; growMask?: number; negativePrompt?: string; seed?: number; outputFormat?: "png" | "jpeg" | "webp"; } interface ReplaceBackgroundAndRelightOptions { backgroundPrompt?: string; backgroundReference?: string; foregroundPrompt?: string; negativePrompt?: string; preserveOriginalSubject?: number; originalBackgroundDepth?: number; keepOriginalBackground?: boolean; lightSourceDirection?: "above" | "below" | "left" | "right"; lightReference?: string; lightSourceStrength?: number; seed?: number; outputFormat?: "png" | "jpeg" | "webp"; } interface ControlStyleOptions { prompt: string; negativePrompt?: string; aspectRatio?: | "16:9" | "1:1" | "21:9" | "2:3" | "3:2" | "4:5" | "5:4" | "9:16" | "9:21"; fidelity?: number; seed?: number; outputFormat?: "png" | "jpeg" | "webp"; } interface ControlStructureOptions { prompt: string; controlStrength?: number; negativePrompt?: string; seed?: number; outputFormat?: "png" | "jpeg" | "webp"; } export class StabilityAiApiClient { private readonly apiKey: string; private readonly baseUrl = "https://api.stability.ai"; private readonly axiosClient: AxiosInstance; constructor(apiKey: string) { this.apiKey = apiKey; this.axiosClient = axios.create({ baseURL: this.baseUrl, timeout: 120000, maxBodyLength: Infinity, maxContentLength: Infinity, headers: { Authorization: `Bearer ${this.apiKey}`, Accept: "application/json", }, }); } // https://platform.stability.ai/docs/api-reference#tag/Generate/paths/~1v2beta~1stable-image~1generate~1core/post async generateImageCore( prompt: string, options?: GenerateImageCoreOptions ): Promise<{ base64Image: string }> { const payload = { prompt, output_format: "png", aspect_ratio: options?.aspectRatio, negative_prompt: options?.negativePrompt, style_preset: options?.stylePreset, ...options, }; return this.axiosClient .postForm( `${this.baseUrl}/v2beta/stable-image/generate/core`, axios.toFormData(payload, new FormData()) ) .then((res) => { const base64Image = res.data.image; return { base64Image, }; }); } async removeBackground( imageFilePath: string ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), output_format: "png", }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/edit/remove-background`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async outpaint( imageFilePath: string, options: OutpaintOptions ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), output_format: options.outputFormat || "png", left: options.left || 0, right: options.right || 0, up: options.up || 0, down: options.down || 0, ...(options.creativity !== undefined && { creativity: options.creativity, }), ...(options.prompt && { prompt: options.prompt }), ...(options.seed && { seed: options.seed }), }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/edit/outpaint`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async searchAndReplace( imageFilePath: string, options: SearchAndReplaceOptions ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), output_format: "png", search_prompt: options.searchPrompt, prompt: options.prompt, }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/edit/search-and-replace`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async upscaleFast(imageFilePath: string): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), output_format: "png", }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/upscale/fast`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error: any) { if (error.response?.status === 400 && error.response?.data?.errors) { const errorMessage = `Invalid parameters: ${error.response.data.errors.join(", ")}`; throw new Error(errorMessage); } throw new Error( `API error (${error.response?.status}): ${JSON.stringify(error.response?.data)}` ); } } async fetchGenerationResult(id: string): Promise<{ base64Image: string }> { try { while (true) { const response = await this.axiosClient.get( `${this.baseUrl}/v2beta/results/${id}`, { headers: { Accept: "application/json", }, } ); if (response.status === 200) { return { base64Image: response.data.result }; } else if (response.status === 202) { // Generation still in progress, wait 10 seconds before polling again await new Promise((resolve) => setTimeout(resolve, 10000)); } else { throw new Error(`Unexpected status: ${response.status}`); } } } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async upscaleCreative( imageFilePath: string, options: UpscaleCreativeOptions ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), prompt: options.prompt, output_format: options.outputFormat || "png", ...(options.negativePrompt && { negative_prompt: options.negativePrompt, }), ...(options.seed !== undefined && { seed: options.seed }), ...(options.creativity !== undefined && { creativity: options.creativity, }), }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/upscale/creative`, axios.toFormData(payload, new FormData()) ); // Get the generation ID from the response const generationId = response.data.id; // Poll for the result return await this.fetchGenerationResult(generationId); } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async controlSketch( imageFilePath: string, options: ControlSketchOptions ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), prompt: options.prompt, output_format: options.outputFormat || "png", ...(options.controlStrength !== undefined && { control_strength: options.controlStrength, }), ...(options.negativePrompt && { negative_prompt: options.negativePrompt, }), ...(options.seed !== undefined && { seed: options.seed }), }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/control/sketch`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async searchAndRecolor( imageFilePath: string, options: SearchAndRecolorOptions ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), prompt: options.prompt, select_prompt: options.selectPrompt, output_format: options.outputFormat || "png", ...(options.growMask !== undefined && { grow_mask: options.growMask }), ...(options.negativePrompt && { negative_prompt: options.negativePrompt, }), ...(options.seed !== undefined && { seed: options.seed }), }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/edit/search-and-recolor`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async replaceBackgroundAndRelight( imageFilePath: string, options: ReplaceBackgroundAndRelightOptions ): Promise<{ base64Image: string }> { const payload = { subject_image: fs.createReadStream(imageFilePath), output_format: options.outputFormat || "png", ...(options.backgroundPrompt && { background_prompt: options.backgroundPrompt, }), ...(options.backgroundReference && { background_reference: fs.createReadStream(options.backgroundReference), }), ...(options.foregroundPrompt && { foreground_prompt: options.foregroundPrompt, }), ...(options.negativePrompt && { negative_prompt: options.negativePrompt, }), ...(options.preserveOriginalSubject !== undefined && { preserve_original_subject: options.preserveOriginalSubject, }), ...(options.originalBackgroundDepth !== undefined && { original_background_depth: options.originalBackgroundDepth, }), ...(options.keepOriginalBackground !== undefined && { keep_original_background: options.keepOriginalBackground, }), ...(options.lightSourceDirection && { light_source_direction: options.lightSourceDirection, }), ...(options.lightReference && { light_reference: fs.createReadStream(options.lightReference), }), ...(options.lightSourceStrength !== undefined && { light_source_strength: options.lightSourceStrength, }), ...(options.seed !== undefined && { seed: options.seed }), }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/edit/replace-background-and-relight`, axios.toFormData(payload, new FormData()) ); // Get the generation ID from the response const generationId = response.data.id; // Poll for the result return await this.fetchGenerationResult(generationId); } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async controlStyle( imageFilePath: string, options: ControlStyleOptions ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), prompt: options.prompt, output_format: options.outputFormat || "png", ...(options.negativePrompt && { negative_prompt: options.negativePrompt, }), ...(options.aspectRatio && { aspect_ratio: options.aspectRatio, }), ...(options.fidelity !== undefined && { fidelity: options.fidelity, }), ...(options.seed !== undefined && { seed: options.seed }), }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/control/style`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } async controlStructure( imageFilePath: string, options: ControlStructureOptions ): Promise<{ base64Image: string }> { const payload = { image: fs.createReadStream(imageFilePath), prompt: options.prompt, output_format: options.outputFormat || "png", ...(options.controlStrength !== undefined && { control_strength: options.controlStrength, }), ...(options.negativePrompt && { negative_prompt: options.negativePrompt, }), ...(options.seed !== undefined && { seed: options.seed }), }; try { const response = await this.axiosClient.postForm( `${this.baseUrl}/v2beta/stable-image/control/structure`, axios.toFormData(payload, new FormData()) ); const base64Image = response.data.image; return { base64Image }; } catch (error) { if (axios.isAxiosError(error) && error.response) { const data = error.response.data; if (error.response.status === 400) { throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); } throw new Error( `API error (${error.response.status}): ${JSON.stringify(data)}` ); } throw error; } } }