├── .github └── CODEOWNERS ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── claude-1.png ├── claude-2.png ├── mcp-server-stability-ai-logo.png ├── prompts.png ├── teaser-1.png ├── teaser-2.png ├── teaser.gif └── teaser.png ├── package-lock.json ├── package.json ├── src ├── env.d.ts ├── gcs │ └── gcsClient.ts ├── index.ts ├── prompts │ └── index.ts ├── resources │ ├── filesystemResourceClient.ts │ ├── gcsResourceClient.ts │ ├── resourceClient.ts │ └── resourceClientFactory.ts ├── sse.ts ├── stabilityAi │ ├── sd35Client.ts │ ├── stabilityAiApiClient.ts │ └── types.ts ├── stdio.ts └── tools │ ├── controlSketch.ts │ ├── controlStructure.ts │ ├── controlStyle.ts │ ├── generateImageCore.ts │ ├── generateImageSD35.ts │ ├── generateImageUltra.ts │ ├── index.ts │ ├── listResources.ts │ ├── outpaint.ts │ ├── removeBackground.ts │ ├── replaceBackgroundAndRelight.ts │ ├── searchAndRecolor.ts │ ├── searchAndReplace.ts │ ├── upscaleCreative.ts │ └── upscaleFast.ts └── tsconfig.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Global owner 2 | * @tadasant -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # project-specific 133 | build 134 | src/test.ts 135 | 136 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.0 4 | 5 | - Add support for Stable Diffusion 3.5 models with the `stability-ai-generate-image-sd35` tool 6 | - Support advanced configuration options for SD3.5: 7 | - Model selection (SD3.5 Large, Medium, Turbo) 8 | - CFG scale parameter 9 | - Various output formats and aspect ratios 10 | - Negative prompts 11 | - Style presets 12 | - Random seed control 13 | 14 | ## 0.1.0 15 | 16 | - Remove base64 encoding approach to saving images to filesystem (wasn't properly functional) 17 | - Remove requirement to set `IMAGE_STORAGE_DIRECTORY` environment variable in favor of reasonable defaults per OS 18 | - Make client come up with meaningful image names for output images 19 | - Remove `stability-ai-0-find-image-file-location` Tool and add `stability-ai-0-list-resources` Tool 20 | - Refactor to avoid using filesystem & Tools directly in favor of Resources abstraction 21 | - Add Prompts capability 22 | - Add features: 23 | - `stability-ai-control-style`: generate an image in the style of a reference image 24 | - `stability-ai-control-structure`: generate an image while maintaining the structure of a reference image 25 | - `stability-ai-replace-background-and-relight`: replace the background of an image and relight it 26 | - `stability-ai-search-and-recolor`: search for and recolor objects in an image 27 | 28 | ## 0.0.3 29 | 30 | - Initial public release 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before starting any work, please open an Issue to discuss the changes you'd like to make; let's make sure we don't duplicate effort. 4 | 5 | Please do all your work on a fork of the repository and open a PR against the main branch. 6 | 7 | ## Running the server locally 8 | 9 | ``` 10 | npm run build 11 | npm run start 12 | ``` 13 | 14 | ## Generating types 15 | 16 | 1. Download the Stability AI OpenAPI schema: https://platform.stability.ai/docs/api-reference 17 | 2. Run the following command to generate the types: 18 | 19 | ``` 20 | npx openapi-typescript openapi.json -o /path/to/mcp-server-stability/src/stabilityAiApi/types.ts 21 | ``` 22 | 23 | ## Debugging tools 24 | 25 | ### Running Inspector 26 | 27 | ``` 28 | npx @modelcontextprotocol/inspector node path/to/mcp-server-stability-ai/build/index.js 29 | ``` 30 | 31 | ### Claude 32 | 33 | #### Follow logs in real-time 34 | 35 | ``` 36 | tail -n 20 -f ~/Library/Logs/Claude/mcp*.log 37 | ``` 38 | 39 | ## Testing with a test.ts file 40 | 41 | Helpful for isolating and trying out pieces of code. 42 | 43 | 1. Create a `src/test.ts` file. 44 | 2. Write something like this in it 45 | 46 | ```ts 47 | import * as dotenv from "dotenv"; 48 | import { StabilityAiApiClient } from "./stabilityAi/stabilityAiApiClient.js"; 49 | import * as fs from "fs"; 50 | 51 | dotenv.config(); 52 | 53 | if (!process.env.STABILITY_AI_API_KEY) { 54 | throw new Error("STABILITY_AI_API_KEY is required in .env file"); 55 | } 56 | 57 | const API_KEY = process.env.STABILITY_AI_API_KEY; 58 | 59 | async function test() { 60 | const client = new StabilityAiApiClient(API_KEY); 61 | const data = await client.generateImageCore( 62 | "A beautiful sunset over mountains" 63 | ); 64 | 65 | // Create the directory if it doesn't exist 66 | fs.mkdirSync("stabilityAi", { recursive: true }); 67 | 68 | // Write data to file 69 | fs.writeFileSync("stabilityAi/test.png", data.base64Image, "base64"); 70 | console.log("Image saved to stabilityAi/test.png"); 71 | } 72 | 73 | test().catch(console.error); 74 | ``` 75 | 76 | 3. `npm run build` and `node build/test.js` 77 | 78 | ## Publishing 79 | 80 | ``` 81 | npm run build 82 | ``` 83 | 84 | Delete any files that shouldn't be published (e.g. `build/test.js`). Then run: 85 | 86 | ``` 87 | npm publish 88 | ``` 89 | 90 | After publishing, tag the GitHub repository with the version from package.json and add release notes: 91 | 92 | ``` 93 | # Get the version from package.json 94 | VERSION=$(node -p "require('./package.json').version") 95 | 96 | # Create an annotated tag with a message 97 | git tag -a v$VERSION -m "Release v$VERSION" 98 | 99 | # Push the tag to the remote repository 100 | git push origin v$VERSION 101 | 102 | # Create a GitHub release with more detailed notes 103 | # You can do this through the GitHub UI: 104 | # 1. Go to https://github.com/tadasant/mcp-server-stability-ai/releases 105 | # 2. Click "Draft a new release" 106 | # 3. Select the tag you just pushed 107 | # 4. Add a title (e.g., "v1.2.0") 108 | # 5. Add detailed release notes describing the changes 109 | # 6. Click "Publish release" 110 | ``` 111 | 112 | TODO: Automate these steps. 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tadas Antanavicius 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |


Stability AI MCP Server

3 | 4 | 5 | Smithery Badge 6 |
7 | 8 |
9 | 10 |
11 | PulseMCP Badge 12 |
13 | 14 |
15 | 16 | Haven't heard about MCP yet? The easiest way to keep up-to-date is to read our [weekly newsletter at PulseMCP](https://www.pulsemcp.com/). 17 | 18 | --- 19 | 20 | This is an MCP ([Model Context Protocol](https://modelcontextprotocol.io/)) Server integrating MCP Clients with [Stability AI](https://stability.ai/)'s latest & greatest Stable Diffusion image manipulation functionalities: generate, edit, upscale, and more. 21 | 22 | Stability AI is a leading AI model provider and this server connects directly to their [hosted REST API](https://platform.stability.ai/docs/api-reference). You will need to sign up for an [API Key from stability.ai](https://platform.stability.ai/account/keys) to get started. 23 | 24 | They provide 25 credits for free. Afterward, [pay-as-you-go pricing](https://platform.stability.ai/pricing) is very reasonable: $0.01/credit, where 3 credits gets you an image generation on their Core model. So 100 high quality images = just $3. 25 | 26 | This project is NOT officially affiliated with Stability AI. 27 | 28 | [Demo video](https://youtu.be/7ceSgVC4ZLs), and a teaser here: 29 | 30 | ![Teaser](https://github.com/tadasant/mcp-server-stability-ai/blob/main/images/teaser.gif) 31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 | # Table of Contents 40 | 41 | - [Highlights](#highlights) 42 | - [Capabilities](#capabilities) 43 | - [Usage Tips](#usage-tips) 44 | - [Examples](#examples) 45 | - [Setup](#setup) 46 | - [Cheatsheet](#cheatsheet) 47 | - [Claude Desktop](#claude-desktop) 48 | - [Manual Setup](#manual-setup) 49 | 50 | # Highlights 51 | 52 | **No learning curve**: This server is designed to use sensible defaults and provide simple, smooth UX for the most common actions related to generating and manipulating image files. You don't need to be technical or understand anything about image models to use it effectively. 53 | 54 | **Chain manipulations**: You can generate an image, then replace an item within it, then remove the background... all in a single Claude conversation. 55 | 56 | **Minimal configuration**: All you need to get started is a Stability AI API key. Set environment variables for that and a local directory path to store output images, and you're ready to go. 57 | 58 | **Leverage the best in class image models**: Stability AI is the leading provider of image models exposed via API. Using this server integrates them into Claude or another MCP client - head and shoulders above an experience like using DALL-E models in ChatGPT. 59 | 60 | # Capabilities 61 | 62 | This server is built and tested on macOS with Claude Desktop. It should work with other MCP clients as well. 63 | 64 | | Tool Name | Description | Estimated Stability API Cost | 65 | | -------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------- | 66 | | `generate-image` | Generate a high quality image of anything based on a provided prompt & other optional parameters. | $0.03 | 67 | | `generate-image-sd35` | Generate an image using Stable Diffusion 3.5 models with advanced configuration options. | $0.04-$0.07 | 68 | | `remove-background` | Remove the background from an image. | $0.02 | 69 | | `outpaint` | Extend an image in any direction while maintaining visual consistency. | $0.04 | 70 | | `search-and-replace` | Replace objects or elements in an image by describing what to replace and what to replace it with. | $0.04 | 71 | | `upscale-fast` | Enhance image resolution by 4x. | $0.01 | 72 | | `upscale-creative` | Enhance image resolution up to 4K. | $0.25 | 73 | | `control-sketch` | Translate hand-drawn sketch to production-grade image. | $0.03 | 74 | | `control-style` | Generate an image in the style of a reference image. | $0.04 | 75 | | `control-structure` | Generate an image while maintaining the structure of a reference image. | $0.03 | 76 | | `replace-background-and-relight` | Replace the background of an image and relight it. | $0.08 | 77 | | `search-and-recolor` | Search for and recolor objects in an image. | $0.05 | 78 | 79 | # Usage Tips 80 | 81 | - All processed images are automatically saved to `IMAGE_STORAGE_DIRECTORY`, opened for preview, and made available as resources 82 | - Do _not_ try to copy/paste or upload image files to Claude. Claude does not store images anywhere, so we cannot work with those with the MCP server. They have to be "uploaded" (saved to) the `IMAGE_STORAGE_DIRECTORY` and then they will show up as resources available in the chat. 83 | - You can use Prompts that come preloaded instead of writing your own verbiage: 84 | 85 | Prompts 86 | 87 | # Examples 88 | 89 | ## Generate an image 90 | 91 | 1. `Generate an image of a cat` 92 | 2. `Generate a photorealistic image of a cat in a cyberpunk city, neon lights reflecting off its fur, 16:9 aspect ratio` 93 | 3. `Generate a detailed digital art piece of a cat wearing a space suit floating through a colorful nebula, style preset: digital-art, aspect ratio: 21:9` 94 | 95 | ## Generate an image with SD3.5 96 | 97 | 1. `Generate an image of a woman with cybernetic wolf ears using the SD3.5 model, with the "neon-punk" style preset` 98 | 2. `Generate an image of a futuristic city using the SD3.5 Large Turbo model, with aspect ratio 16:9` 99 | 3. `Generate an image of an astronaut on mars using the SD3.5 Large model, with cfg scale 7.5, "analog-film" style preset, and seed 42` 100 | 101 | ## Remove background 102 | 103 | 1. `Remove the background from the image I just generated` 104 | 2. `Remove the background from product-photo.jpg to prepare it for my e-commerce site` 105 | 3. `Remove the background from group-photo.png so I can composite it with another image` 106 | 107 | ## Outpaint (Uncrop) 108 | 109 | 1. `Extend vacation-photo.jpg 500 pixels to the right to include more of the beach` 110 | 2. `Extend family-portrait.png 300 pixels up to show more of the mountains, and 200 pixels right to include more landscape` 111 | 3. `Extend artwork.png in all directions to create a wider fantasy forest scene that matches the original environment` 112 | 113 | ## Search and Replace 114 | 115 | 1. `In my last image, replace the red car with a blue car` 116 | 2. `In portrait.png, replace the plain background with a sunset over mountains` 117 | 3. `In landscape.jpg, replace the modern buildings with victorian-era architecture while maintaining the same atmosphere` 118 | 119 | ## Upscale 120 | 121 | 1. `Upscale profile-pic.jpg for better resolution` 122 | 2. `Upscale product-photo.png to make it print-ready` 123 | 124 | And then, if the output still isn't good enough, you can upscale it again: 125 | 126 | 1. `Try again with better quality` 127 | 128 | ## Control Sketch 129 | 130 | 1. `Transform sketch.png into a colored illustration for a children's book` 131 | 2. `Convert wireframe.jpg into a detailed 3D render for a modern architectural visualization` 132 | 133 | ## Control Style 134 | 135 | 1. `Generate an image in the style of the reference image` 136 | 137 | ## Control Structure 138 | 139 | 1. `Generate an image while maintaining the structure of the reference image` 140 | 141 | ## Replace Background and Relight 142 | 143 | 1. `Replace the background of the image I just generated with a sunset over mountains` 144 | 145 | ## Search and Recolor 146 | 147 | 1. `In my last image, make the red car be blue instead` 148 | 149 | # Setup 150 | 151 | ## Metadata Logging 152 | 153 | The server can save metadata from image generation requests to help with tracking and troubleshooting. 154 | 155 | | Environment Variable | Description | Required | Default Value | 156 | | ------------------------ | --------------------------------------------------- | -------- | ------------- | 157 | | `SAVE_METADATA` | Save metadata for successful image generations | N | `true` | 158 | | `SAVE_METADATA_FAILED` | Save metadata for failed image generations | N | `false` | 159 | 160 | When enabled, a `.txt` file with the same name as the generated image will be created in the same directory. This file contains: 161 | 162 | - Timestamp of the request 163 | - All request parameters (prompt, model, style preset, etc.) 164 | - Response information (success status, generation time) 165 | 166 | This file will also be created for failed requests if `SAVE_METADATA_FAILED` is enabled. 167 | 168 | ## Cheatsheet 169 | 170 | | Environment Variable | Description | Required | Default Value | Example | 171 | | ------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | 172 | | `STABILITY_AI_API_KEY` | Your Stability AI API key. Get one at [platform.stability.ai](https://platform.stability.ai/account/keys) | Y | N/A | `sk-1234567890` | 173 | | `IMAGE_STORAGE_DIRECTORY` | Directory where generated images will be saved | N | `/tmp/tadasant-mcp-server-stability-ai` OR `C:\\Windows\\Temp\\mcp-server-stability-ai` | `/Users/admin/Downloads/stability-ai-images` (Mac OS/Linux), `C:\\Users\\Admin\\Downloads\\stability-ai-images` (Windows)| 174 | | `SAVE_METADATA` | Save metadata for successful image generations | N | `true` | `true` or `false` | 175 | | `SAVE_METADATA_FAILED` | Save metadata for failed image generations | N | `true` | `true` or `false` | 176 | | `GCS_PROJECT_ID` | Google Cloud Project ID for storing images | N (Y if using SSE) | N/A | `your-project-id` | 177 | | `GCS_CLIENT_EMAIL` | Google Cloud Service Account client email for storing images | N (Y if using SSE) | N/A | `your-service-account@project.iam.gserviceaccount.com` | 178 | | `GCS_PRIVATE_KEY` | Google Cloud Service Account private key for storing images | N (Y if using SSE) | N/A | `-----BEGIN PRIVATE KEY-----\nYourKeyHere\n-----END PRIVATE KEY-----\n` | 179 | | `GCS_BUCKET_NAME` | Google Cloud Storage bucket name for storing images | N (Y if using SSE) | N/A | `your-bucket-name` | 180 | 181 | ## Claude Desktop 182 | 183 | If you prefer a video tutorial, here's [a quick one](https://youtu.be/7ceSgVC4ZLs). 184 | 185 | Create a folder directory somewhere on your machine to store generated/modified images. Some options: 186 | 187 | - `/Users//Downloads/stability-ai-images` 188 | - `/Users//Library/Application Support/Claude/mcp-server-stability-ai/images` 189 | 190 | And make sure you have an [API key from Stability AI](https://platform.stability.ai/account/keys). 191 | 192 | Then proceed to your preferred method of configuring the server below. If this is your first time using MCP Servers, you'll want to make sure you have the [Claude Desktop application](https://claude.ai/download) and follow the [official MCP setup instructions](https://modelcontextprotocol.io/quickstart/user). 193 | 194 | ### Manual Setup 195 | 196 | You're going to need Node working on your machine so you can run `npx` commands in your terminal. If you don't have Node, you can install it from [nodejs.org](https://nodejs.org/en/download). 197 | 198 | macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 199 | 200 | Windows: `%APPDATA%\Claude\claude_desktop_config.json` 201 | 202 | Modify your `claude_desktop_config.json` file to add the following: 203 | 204 | ``` 205 | { 206 | "mcpServers": { 207 | "stability-ai": { 208 | "command": "npx", 209 | "args": [ 210 | "-y", 211 | "mcp-server-stability-ai" 212 | ], 213 | "env": { 214 | "STABILITY_AI_API_KEY": "sk-1234567890" 215 | } 216 | } 217 | } 218 | } 219 | ``` 220 | 221 | Restart Claude Desktop and you should be ready to go: 222 | 223 | Claude First Image 224 | 225 | Claude Second Image 226 | 227 | ### Installing via Smithery 228 | 229 | To install for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-server-stability-ai): 230 | 231 | ```bash 232 | npx @smithery/cli install mcp-server-stability-ai --client claude 233 | ``` 234 | 235 | ## SSE Mode 236 | 237 | This server has the option to run in SSE mode by starting it with the following command: 238 | 239 | ```bash 240 | npx mcp-server-stability-ai -y --sse 241 | ``` 242 | 243 | This mode is useful if you intend to deploy this server for third party usage over HTTP. 244 | 245 | You will need to set the `GCS_PROJECT_ID`, `GCS_CLIENT_EMAIL`, `GCS_BUCKET_NAME`, and `GCS_PRIVATE_KEY` environment variables, because the server will store image files in Google Cloud Storage instead of its local filesystem. 246 | 247 | Note that the scheme for multitenancy is very naive and insecure: it uses the requestor's IP address to segment the GCS prefixes used to the store the images, and makes all images publicly accessible in order to communicate them back to the MCP client. So in theory, if someone knows your IP address and then name(s) of files you generated, they could access your images by guessing the URL. 248 | 249 | ## Roadmap 250 | 251 | Recently completed: 252 | - ✅ Added support for Stable Diffusion 3.5 models 253 | - ✅ Added support for Stable Image Ultra 254 | - ✅ Added metadata logging for image generation requests 255 | 256 | These are coming soon; but PR's are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md). 257 | 258 | - `inpaint` - A more precise version of `search-and-x` functionalities, requires mananging a mask to define to area to replace. 259 | - Base image manipulation (crop, rotate, resize, etc.): likely as its own MCP server 260 | - Ability to inpaint one image into another image. Doesn't seem possible with Stability API; will probably want another MCP server hitting a different API to accomplish this. 261 | - MCP client custom-made for image manipulation 262 | 263 | # Contributing 264 | 265 | External contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. 266 | 267 | Also please feel free to raise issues or feature requests; love seeing how people are using this and how it could be made better. 268 | -------------------------------------------------------------------------------- /images/claude-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/claude-1.png -------------------------------------------------------------------------------- /images/claude-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/claude-2.png -------------------------------------------------------------------------------- /images/mcp-server-stability-ai-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/mcp-server-stability-ai-logo.png -------------------------------------------------------------------------------- /images/prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/prompts.png -------------------------------------------------------------------------------- /images/teaser-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/teaser-1.png -------------------------------------------------------------------------------- /images/teaser-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/teaser-2.png -------------------------------------------------------------------------------- /images/teaser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/teaser.gif -------------------------------------------------------------------------------- /images/teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tadasant/mcp-server-stability-ai/6a12dd79f82329ae2d3c14b06c9a3a4d0a627817/images/teaser.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "bin": { 4 | "mcp-server-stability-ai": "./build/index.js" 5 | }, 6 | "scripts": { 7 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 8 | "start:test": "npm run build && node build/test.js" 9 | }, 10 | "files": [ 11 | "build" 12 | ], 13 | "dependencies": { 14 | "@google-cloud/storage": "^7.15.0", 15 | "@modelcontextprotocol/sdk": "^1.0.4", 16 | "axios": "^1.7.9", 17 | "body-parser": "^1.20.3", 18 | "dotenv": "^16.4.7", 19 | "express": "^4.21.2", 20 | "open": "^10.1.0", 21 | "zod": "^3.24.1" 22 | }, 23 | "name": "mcp-server-stability-ai", 24 | "version": "0.3.0", 25 | "description": "MCP [Model Context Protocol](https://modelcontextprotocol.io/) Server integrating MCP Clients with [Stability AI](https://stability.ai/) image manipulation functionalities: generate, edit, upscale, and more.", 26 | "main": "index.js", 27 | "keywords": [ 28 | "mcp", 29 | "model context protocol", 30 | "mcp server", 31 | "stability", 32 | "stability ai", 33 | "image generation", 34 | "image manipulation", 35 | "upscale" 36 | ], 37 | "author": "Tadas Antanavicius (https://github.com/tadasant)", 38 | "license": "MIT", 39 | "devDependencies": { 40 | "@types/dotenv": "^6.1.1", 41 | "@types/express": "^5.0.0", 42 | "@types/node": "^22.10.2", 43 | "typescript": "^5.7.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | STABILITY_AI_API_KEY: string; 5 | IMAGE_STORAGE_DIRECTORY: string; 6 | [key: string]: string | undefined; 7 | } 8 | } 9 | } 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /src/gcs/gcsClient.ts: -------------------------------------------------------------------------------- 1 | import { Storage, File } from "@google-cloud/storage"; 2 | 3 | // Example .env file: 4 | // GCS_PROJECT_ID=your-project-id 5 | // GCS_CLIENT_EMAIL=your-service-account@project.iam.gserviceaccount.com 6 | // GCS_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nYourKeyHere\n-----END PRIVATE KEY-----\n 7 | interface GcsClientConfig { 8 | privateKey?: string; 9 | clientEmail?: string; 10 | projectId?: string; 11 | bucketName?: string; 12 | } 13 | 14 | interface UploadOptions { 15 | contentType?: string; 16 | destination?: string; 17 | } 18 | 19 | export class GcsClient { 20 | private readonly storage: Storage; 21 | bucketName: string; 22 | 23 | constructor(config?: GcsClientConfig) { 24 | this.bucketName = config?.bucketName as string; 25 | const credentials = 26 | config?.privateKey && config?.clientEmail 27 | ? { 28 | type: "service_account", 29 | private_key: config.privateKey.replace(/\\n/g, "\n"), 30 | client_email: config.clientEmail, 31 | project_id: config.projectId, 32 | } 33 | : undefined; 34 | 35 | this.storage = new Storage({ 36 | credentials, 37 | projectId: config?.projectId, 38 | }); 39 | 40 | // Initialize bucket 41 | this.initializeBucket().catch((error) => { 42 | console.error("Warning: Bucket initialization error:", error.message); 43 | }); 44 | } 45 | 46 | private async initializeBucket(): Promise { 47 | try { 48 | const [exists] = await this.storage.bucket(this.bucketName).exists(); 49 | if (!exists) { 50 | await this.storage.createBucket(this.bucketName); 51 | console.log(`Bucket ${this.bucketName} created successfully.`); 52 | } else { 53 | console.log(`Bucket ${this.bucketName} already exists.`); 54 | } 55 | } catch (error) { 56 | throw new Error(`Failed to initialize bucket: ${error}`); 57 | } 58 | } 59 | 60 | async uploadFile(filePath: string, options?: UploadOptions): Promise { 61 | try { 62 | const bucket = this.storage.bucket(this.bucketName); 63 | const destination = options?.destination || filePath.split("/").pop(); 64 | 65 | const [file] = await bucket.upload(filePath, { 66 | destination, 67 | contentType: options?.contentType, 68 | public: true, 69 | }); 70 | 71 | return file; 72 | } catch (error) { 73 | throw new Error(`Failed to upload file: ${error}`); 74 | } 75 | } 76 | 77 | async downloadFile(fileName: string, destinationPath: string): Promise { 78 | try { 79 | const bucket = this.storage.bucket(this.bucketName); 80 | const file = bucket.file(fileName); 81 | 82 | await file.download({ 83 | destination: destinationPath, 84 | }); 85 | } catch (error) { 86 | throw new Error(`Failed to download file: ${error}`); 87 | } 88 | } 89 | 90 | async listFiles(prefix?: string): Promise { 91 | try { 92 | const bucket = this.storage.bucket(this.bucketName); 93 | const [files] = await bucket.getFiles({ prefix }); 94 | return files; 95 | } catch (error) { 96 | throw new Error(`Failed to list files: ${error}`); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { 5 | CallToolRequestSchema, 6 | GetPromptRequestSchema, 7 | ListPromptsRequestSchema, 8 | ListResourcesRequestSchema, 9 | ListToolsRequestSchema, 10 | ReadResourceRequestSchema, 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | import { z } from "zod"; 13 | import * as dotenv from "dotenv"; 14 | import { 15 | generateImage, 16 | GenerateImageArgs, 17 | generateImageToolDefinition, 18 | generateImageCore, 19 | generateImageCoreArgs, 20 | GenerateImageCoreArgs, 21 | generateImageCoreToolDefinition, 22 | generateImageUltra, 23 | GenerateImageUltraArgs, 24 | generateImageUltraToolDefinition, 25 | generateImageSD35, 26 | GenerateImageSD35Args, 27 | generateImageSD35ToolDefinition, 28 | removeBackground, 29 | RemoveBackgroundArgs, 30 | removeBackgroundToolDefinition, 31 | outpaint, 32 | OutpaintArgs, 33 | outpaintToolDefinition, 34 | searchAndReplace, 35 | SearchAndReplaceArgs, 36 | searchAndReplaceToolDefinition, 37 | upscaleFast, 38 | UpscaleFastArgs, 39 | upscaleFastToolDefinition, 40 | upscaleCreative, 41 | UpscaleCreativeArgs, 42 | upscaleCreativeToolDefinition, 43 | controlSketch, 44 | ControlSketchArgs, 45 | controlSketchToolDefinition, 46 | listResources, 47 | listResourcesToolDefinition, 48 | searchAndRecolor, 49 | SearchAndRecolorArgs, 50 | searchAndRecolorToolDefinition, 51 | replaceBackgroundAndRelight, 52 | ReplaceBackgroundAndRelightArgs, 53 | replaceBackgroundAndRelightToolDefinition, 54 | controlStyle, 55 | ControlStyleArgs, 56 | controlStyleToolDefinition, 57 | controlStructure, 58 | ControlStructureArgs, 59 | controlStructureToolDefinition, 60 | } from "./tools/index.js"; 61 | import { 62 | initializeResourceClient, 63 | ResourceClientConfig, 64 | getResourceClient, 65 | } from "./resources/resourceClientFactory.js"; 66 | import { prompts, injectPromptTemplate } from "./prompts/index.js"; 67 | import { runSSEServer } from "./sse.js"; 68 | import { runStdioServer } from "./stdio.js"; 69 | import { ResourceContext } from "./resources/resourceClient.js"; 70 | 71 | dotenv.config(); 72 | 73 | if (!process.env.IMAGE_STORAGE_DIRECTORY) { 74 | if (process.platform === "win32") { 75 | // Windows 76 | process.env.IMAGE_STORAGE_DIRECTORY = 77 | "C:\\Windows\\Temp\\mcp-server-stability-ai"; 78 | } else { 79 | // macOS or Linux 80 | process.env.IMAGE_STORAGE_DIRECTORY = 81 | "/tmp/tadasant-mcp-server-stability-ai"; 82 | } 83 | } 84 | 85 | // Set default values for metadata saving 86 | if (process.env.SAVE_METADATA === undefined) { 87 | process.env.SAVE_METADATA = "true"; 88 | } 89 | 90 | if (process.env.SAVE_METADATA_FAILED === undefined) { 91 | process.env.SAVE_METADATA_FAILED = "true"; 92 | } 93 | 94 | if (!process.env.STABILITY_AI_API_KEY) { 95 | throw new Error("STABILITY_AI_API_KEY is a required environment variable"); 96 | } 97 | 98 | const server = new Server( 99 | { 100 | name: "stability-ai", 101 | version: "0.0.1", 102 | }, 103 | { 104 | capabilities: { 105 | tools: {}, 106 | resources: {}, 107 | prompts: {}, 108 | }, 109 | } 110 | ); 111 | 112 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 113 | return { 114 | prompts: prompts.map((p) => ({ 115 | name: p.name, 116 | description: p.description, 117 | })), 118 | }; 119 | }); 120 | 121 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 122 | const { name, arguments: args } = request.params; 123 | 124 | const prompt = prompts.find((p) => p.name === name); 125 | if (!prompt) { 126 | throw new Error(`Prompt not found: ${name}`); 127 | } 128 | 129 | const result = injectPromptTemplate(prompt.template, args); 130 | return { 131 | messages: [ 132 | { 133 | role: "user", 134 | content: { 135 | type: "text", 136 | text: result, 137 | }, 138 | }, 139 | ], 140 | }; 141 | }); 142 | 143 | server.setRequestHandler(ListResourcesRequestSchema, async (request) => { 144 | const meta = request.params?._meta; 145 | const ipAddress = meta?.ip as string; 146 | const context: ResourceContext = { 147 | requestorIpAddress: ipAddress, 148 | }; 149 | 150 | const resourceClient = getResourceClient(); 151 | const resources = await resourceClient.listResources(context); 152 | 153 | return { 154 | resources, 155 | }; 156 | }); 157 | 158 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 159 | const { _meta: meta } = request.params; 160 | const ipAddress = meta?.ip as string; 161 | const context: ResourceContext = { 162 | requestorIpAddress: ipAddress, 163 | }; 164 | 165 | const resourceClient = getResourceClient(); 166 | const resource = await resourceClient.readResource( 167 | request.params.uri, 168 | context 169 | ); 170 | 171 | return { 172 | contents: [resource], 173 | }; 174 | }); 175 | 176 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 177 | const { name, arguments: args, _meta: meta } = request.params; 178 | 179 | const ipAddress = meta?.ip as string; 180 | const context: ResourceContext = { 181 | requestorIpAddress: ipAddress, 182 | }; 183 | 184 | try { 185 | switch (name) { 186 | case generateImageToolDefinition.name: 187 | return generateImage(args as GenerateImageArgs, context); 188 | case generateImageCoreToolDefinition.name: 189 | return generateImageCore(args as generateImageCoreArgs, context); 190 | case generateImageUltraToolDefinition.name: 191 | return generateImageUltra(args as GenerateImageUltraArgs, context); 192 | case generateImageSD35ToolDefinition.name: 193 | return generateImageSD35(args as GenerateImageSD35Args, context); 194 | case removeBackgroundToolDefinition.name: 195 | return removeBackground(args as RemoveBackgroundArgs, context); 196 | case outpaintToolDefinition.name: 197 | return outpaint(args as OutpaintArgs, context); 198 | case searchAndReplaceToolDefinition.name: 199 | return searchAndReplace(args as SearchAndReplaceArgs, context); 200 | case upscaleFastToolDefinition.name: 201 | return upscaleFast(args as UpscaleFastArgs, context); 202 | case upscaleCreativeToolDefinition.name: 203 | return upscaleCreative(args as UpscaleCreativeArgs, context); 204 | case controlSketchToolDefinition.name: 205 | return controlSketch(args as ControlSketchArgs, context); 206 | case listResourcesToolDefinition.name: 207 | return listResources(context); 208 | case searchAndRecolorToolDefinition.name: 209 | return searchAndRecolor(args as SearchAndRecolorArgs, context); 210 | case replaceBackgroundAndRelightToolDefinition.name: 211 | return replaceBackgroundAndRelight( 212 | args as ReplaceBackgroundAndRelightArgs, 213 | context 214 | ); 215 | case controlStyleToolDefinition.name: 216 | return controlStyle(args as ControlStyleArgs, context); 217 | case controlStructureToolDefinition.name: 218 | return controlStructure(args as ControlStructureArgs, context); 219 | default: 220 | throw new Error(`Unknown tool: ${name}`); 221 | } 222 | } catch (error) { 223 | if (error instanceof z.ZodError) { 224 | throw new Error( 225 | `Invalid arguments: ${error.errors 226 | .map((e) => `${e.path.join(".")}: ${e.message}`) 227 | .join(", ")}` 228 | ); 229 | } 230 | throw error; 231 | } 232 | }); 233 | 234 | // List available tools 235 | server.setRequestHandler(ListToolsRequestSchema, async () => { 236 | return { 237 | tools: [ 238 | generateImageToolDefinition, 239 | generateImageCoreToolDefinition, 240 | generateImageUltraToolDefinition, 241 | generateImageSD35ToolDefinition, 242 | removeBackgroundToolDefinition, 243 | outpaintToolDefinition, 244 | searchAndReplaceToolDefinition, 245 | upscaleFastToolDefinition, 246 | upscaleCreativeToolDefinition, 247 | controlSketchToolDefinition, 248 | listResourcesToolDefinition, 249 | searchAndRecolorToolDefinition, 250 | replaceBackgroundAndRelightToolDefinition, 251 | controlStyleToolDefinition, 252 | controlStructureToolDefinition, 253 | ], 254 | }; 255 | }); 256 | 257 | function printUsage() { 258 | console.error("Usage: node build/index.js [--sse]"); 259 | console.error("Options:"); 260 | console.error(" --sse Use SSE transport instead of stdio"); 261 | } 262 | 263 | async function main() { 264 | const args = process.argv.slice(2); 265 | 266 | if (args.length > 1 || (args.length === 1 && args[0] !== "--sse")) { 267 | printUsage(); 268 | throw new Error("Invalid arguments"); 269 | } 270 | 271 | const useSSE = args.includes("--sse"); 272 | 273 | const resourceClientConfig: ResourceClientConfig = useSSE 274 | ? { 275 | type: "gcs", 276 | gcsConfig: { 277 | privateKey: process.env.GCS_PRIVATE_KEY, 278 | clientEmail: process.env.GCS_CLIENT_EMAIL, 279 | projectId: process.env.GCS_PROJECT_ID, 280 | bucketName: process.env.GCS_BUCKET_NAME, 281 | }, 282 | } 283 | : { 284 | type: "filesystem", 285 | imageStorageDirectory: process.env.IMAGE_STORAGE_DIRECTORY!, 286 | }; 287 | 288 | initializeResourceClient(resourceClientConfig); 289 | 290 | if (useSSE) { 291 | await runSSEServer(server); 292 | } else { 293 | await runStdioServer(server); 294 | } 295 | } 296 | 297 | main().catch((error) => { 298 | console.error("Fatal error:", error); 299 | process.exit(1); 300 | }); 301 | -------------------------------------------------------------------------------- /src/prompts/index.ts: -------------------------------------------------------------------------------- 1 | import { controlSketchToolDefinition } from "../tools/controlSketch.js"; 2 | import { controlStyleToolDefinition } from "../tools/controlStyle.js"; 3 | import { generateImageToolDefinition } from "../tools/generateImage.js"; 4 | import { listResourcesToolDefinition } from "../tools/listResources.js"; 5 | import { searchAndReplaceToolDefinition } from "../tools/searchAndReplace.js"; 6 | import { upscaleCreativeToolDefinition } from "../tools/upscaleCreative.js"; 7 | import { upscaleFastToolDefinition } from "../tools/upscaleFast.js"; 8 | import { controlStructureToolDefinition } from "../tools/controlStructure.js"; 9 | 10 | // Expect template to have {{variable}} entries and replace them with args[variable] 11 | export const injectPromptTemplate = ( 12 | template: string, 13 | args: Record | undefined 14 | ) => { 15 | if (!args) { 16 | return template; 17 | } 18 | 19 | return template.replace(/{{(.*?)}}/g, (match, p1) => args[p1] || match); 20 | }; 21 | 22 | export const prompts = [ 23 | { 24 | name: "generate-image-from-text", 25 | description: 26 | "Generate a new image with configurable description, style, and aspect ratio", 27 | template: `Generate an image for the user using ${generateImageToolDefinition.name}. Make sure to ask the user for feedback after the generation.`, 28 | }, 29 | { 30 | name: "generate-image-from-sketch", 31 | description: "Generate an image from a hand-drawn sketch", 32 | template: `The user should provide an image name or location of a sketch image that matches a resource from ${listResourcesToolDefinition.name} (if the results from this tool are not in recent conversation history, run it again so you have an up-to-date list of resources). Try using ${controlSketchToolDefinition.name} to generate an image from the indicated sketch. Make sure to ask the user for feedback after the generation.`, 33 | }, 34 | { 35 | name: "generate-image-in-the-style-of", 36 | description: "Generate an image in the style of an existing image", 37 | template: `The user should provide an image name or location that matches a resource from ${listResourcesToolDefinition.name} (if the results from this tool are not in recent conversation history, run it again so you have an up-to-date list of resources). Try using ${controlStyleToolDefinition.name} to generate an image in the style of the indicated image. Make sure to ask the user for feedback after the generation.`, 38 | }, 39 | { 40 | name: "generate-image-using-structure", 41 | description: 42 | "Generate an image while maintaining the structure (i.e. background, context) of a reference image", 43 | template: `The user should provide an image name or location that matches a resource from ${listResourcesToolDefinition.name} (if the results from this tool are not in recent conversation history, run it again so you have an up-to-date list of resources). Try using ${controlStructureToolDefinition.name} to generate an image that maintains the structure of the indicated image. Make sure to ask the user for feedback after the generation.`, 44 | }, 45 | { 46 | name: "upscale-image", 47 | description: "Upscale the quality of an image", 48 | template: `The user should provide an image name or location that matches a resource from ${listResourcesToolDefinition.name} (if the results from this tool are not in recent conversation history, run it again so you have an up-to-date list of resources). Try using ${upscaleFastToolDefinition.name} to upscale the indicated image. Ask the user what they think of the result. If it's not good enough, then try again with ${upscaleCreativeToolDefinition.name} on the ORIGINAL image. Make sure to ask the user for feedback after the upscaling.`, 49 | }, 50 | { 51 | name: "edit-image", 52 | description: "Make a minor modification to an existing image", 53 | template: `The user should provide an image name or location that matches a resource from ${listResourcesToolDefinition.name} (if the results from this tool are not in recent conversation history, run it again so you have an up-to-date list of resources). 54 | 55 | At this time, we can only perform two kinds of changes: 56 | - "search and replace": the user must provide some object in the image to "search for" and some object in the image to "replace with" 57 | - "search and recolor": the user must provide some object(s) in the image to "search for" and some color(s) to "recolor with" 58 | - "remove background": self explanatory; we attempt to make the background of the image transparent 59 | - "replace background and relight": the user must provide context for what kind of new background they want either as text or as a reference image (which we'll need to grab the URI for) 60 | 61 | Examples of invalid changes we cannot perform at this time: 62 | - Add {object} (without removing anything) 63 | - Tweak {object} (in a way we cannot rephrase to replace it altogether) 64 | 65 | If the user provided something like this, then we should not proceed; inform the user we can only do "search and replace" or "remove background" changes. 66 | 67 | Make sure to ask the user for feedback after any generation attempt.`, 68 | }, 69 | ]; 70 | -------------------------------------------------------------------------------- /src/resources/filesystemResourceClient.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "@modelcontextprotocol/sdk/types.js"; 2 | import { ResourceClient } from "./resourceClient.js"; 3 | import * as fs from "fs"; 4 | 5 | export class FilesystemResourceClient extends ResourceClient { 6 | constructor(private readonly imageStorageDirectory: string) { 7 | super(); 8 | } 9 | 10 | async listResources(): Promise { 11 | const resources = await fs.promises.readdir(this.imageStorageDirectory, { 12 | withFileTypes: true, 13 | }); 14 | 15 | return resources.map((resource) => { 16 | const uri = `file://${this.imageStorageDirectory}/${resource.name}`; 17 | const mimeType = this.getMimeType(resource.name); 18 | return { uri, name: resource.name, mimeType }; 19 | }); 20 | } 21 | 22 | async readResource(uri: string): Promise { 23 | try { 24 | const filePath = uri.replace("file://", ""); 25 | 26 | if (!fs.existsSync(filePath)) { 27 | throw new Error("Resource file not found"); 28 | } 29 | 30 | const content = await fs.promises.readFile(filePath); 31 | const name = filePath.split("/").pop(); 32 | 33 | if (!name) { 34 | throw new Error("Invalid file path"); 35 | } 36 | 37 | const base64Content = Buffer.from(content).toString("base64"); 38 | 39 | return { 40 | uri, 41 | name, 42 | blob: base64Content, 43 | mimeType: this.getMimeType(name), 44 | }; 45 | } catch (error) { 46 | if (error instanceof Error) { 47 | throw new Error(`Failed to read resource: ${error.message}`); 48 | } 49 | throw new Error("Failed to read resource: Unknown error"); 50 | } 51 | } 52 | 53 | async createResource(uri: string, base64image: string): Promise { 54 | const filename = uri.split("/").pop(); 55 | if (!filename) { 56 | throw new Error("Invalid file path"); 57 | } 58 | 59 | const [name, ext] = filename.split("."); 60 | let finalFilename = filename; 61 | 62 | if (fs.existsSync(`${this.imageStorageDirectory}/${filename}`)) { 63 | const randomString = Math.random().toString(36).substring(2, 7); 64 | finalFilename = `${name}-${randomString}.${ext}`; 65 | } 66 | 67 | fs.writeFileSync( 68 | `${this.imageStorageDirectory}/${finalFilename}`, 69 | base64image, 70 | "base64" 71 | ); 72 | 73 | const fullUri = `file://${this.imageStorageDirectory}/${finalFilename}`; 74 | 75 | return { 76 | uri: fullUri, 77 | name: finalFilename, 78 | mimeType: this.getMimeType(finalFilename), 79 | text: `Image ${finalFilename} successfully created at URI ${fullUri}.`, 80 | }; 81 | } 82 | 83 | async resourceToFile(uri: string): Promise { 84 | const filename = uri.split("/").pop(); 85 | if (!filename) { 86 | throw new Error("Invalid file path"); 87 | } 88 | return uri.replace("file://", ""); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/resources/gcsResourceClient.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "@modelcontextprotocol/sdk/types.js"; 2 | import { ResourceClient, ResourceContext } from "./resourceClient.js"; 3 | import { GcsClient } from "../gcs/gcsClient.js"; 4 | import * as fs from "fs"; 5 | import * as os from "os"; 6 | import * as path from "path"; 7 | 8 | export class GcsResourceClient extends ResourceClient { 9 | private readonly tempDir: string; 10 | private ipAddress?: string; 11 | 12 | constructor(private readonly gcsClient: GcsClient) { 13 | super(); 14 | this.tempDir = fs.mkdtempSync( 15 | path.join(os.tmpdir(), "stability-ai-mcp-server-gcs-resource-") 16 | ); 17 | } 18 | 19 | getPrefix(context?: ResourceContext): string { 20 | return context?.requestorIpAddress + "/"; 21 | } 22 | 23 | filenameToUri(filename: string, context?: ResourceContext): string { 24 | return `https://storage.googleapis.com/${this.gcsClient.bucketName}/${this.getPrefix(context)}${filename}`; 25 | } 26 | 27 | uriToFilename(uri: string, context?: ResourceContext): string { 28 | return uri.replace( 29 | `https://storage.googleapis.com/${this.gcsClient.bucketName}/${this.getPrefix(context)}`, 30 | "" 31 | ); 32 | } 33 | 34 | async listResources(context?: ResourceContext): Promise { 35 | const files = await this.gcsClient.listFiles(this.getPrefix(context)); 36 | return files.map((file) => { 37 | const uri = this.filenameToUri(file.name, context); 38 | const nameWithoutPrefix = file.name.replace(this.getPrefix(context), ""); 39 | return { 40 | uri, 41 | name: nameWithoutPrefix, 42 | mimeType: this.getMimeType(nameWithoutPrefix), 43 | }; 44 | }); 45 | } 46 | 47 | async readResource( 48 | uri: string, 49 | context?: ResourceContext 50 | ): Promise { 51 | try { 52 | const filename = this.uriToFilename(uri, context); 53 | const tempFilePath = path.join(this.tempDir, filename); 54 | 55 | await this.gcsClient.downloadFile( 56 | this.getPrefix(context) + filename, 57 | tempFilePath 58 | ); 59 | const content = await fs.promises.readFile(tempFilePath); 60 | const base64Content = content.toString("base64"); 61 | 62 | // Clean up temp file 63 | fs.unlinkSync(tempFilePath); 64 | 65 | return { 66 | uri, 67 | name: filename, 68 | blob: base64Content, 69 | mimeType: this.getMimeType(filename), 70 | }; 71 | } catch (error) { 72 | if (error instanceof Error) { 73 | throw new Error(`Failed to read resource: ${error.message}`); 74 | } 75 | throw new Error("Failed to read resource: Unknown error"); 76 | } 77 | } 78 | 79 | async createResource( 80 | uri: string, 81 | base64image: string, 82 | context?: ResourceContext 83 | ): Promise { 84 | const filename = this.uriToFilename(uri, context); 85 | if (!filename) { 86 | throw new Error("Invalid file path"); 87 | } 88 | 89 | const [name, ext] = filename.split("."); 90 | const randomString = Math.random().toString(36).substring(2, 7); 91 | const finalFilename = `${name}-${randomString}.${ext}`; 92 | 93 | // Write to temp file first 94 | const tempFilePath = path.join(this.tempDir, finalFilename); 95 | fs.writeFileSync(tempFilePath, base64image, "base64"); 96 | 97 | // Upload to GCS 98 | await this.gcsClient.uploadFile(tempFilePath, { 99 | destination: this.getPrefix(context) + finalFilename, 100 | contentType: this.getMimeType(finalFilename), 101 | }); 102 | 103 | // Clean up temp file 104 | fs.unlinkSync(tempFilePath); 105 | 106 | const fullUri = this.filenameToUri(finalFilename, context); 107 | 108 | return { 109 | uri: fullUri, 110 | name: finalFilename, 111 | mimeType: this.getMimeType(finalFilename), 112 | text: `Image ${finalFilename} successfully created at URI ${fullUri}.`, 113 | }; 114 | } 115 | 116 | async resourceToFile( 117 | uri: string, 118 | context?: ResourceContext 119 | ): Promise { 120 | const filename = this.uriToFilename(uri, context); 121 | if (!filename) { 122 | throw new Error("Invalid file path"); 123 | } 124 | 125 | const tempFilePath = path.join(this.tempDir, filename); 126 | await this.gcsClient.downloadFile( 127 | this.getPrefix(context) + filename, 128 | tempFilePath 129 | ); 130 | 131 | return tempFilePath; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/resources/resourceClient.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "@modelcontextprotocol/sdk/types.js"; 2 | 3 | export abstract class ResourceClient { 4 | abstract listResources(context?: ResourceContext): Promise; 5 | abstract readResource(uri: string, context?: ResourceContext): Promise; 6 | abstract createResource(uri: string, base64image: string, context?: ResourceContext): Promise; 7 | abstract resourceToFile(uri: string, context?: ResourceContext): Promise; 8 | 9 | protected getMimeType(filename: string): string { 10 | const ext = filename.toLowerCase().split(".").pop(); 11 | switch (ext) { 12 | case "jpg": 13 | case "jpeg": 14 | return "image/jpeg"; 15 | case "png": 16 | return "image/png"; 17 | case "gif": 18 | return "image/gif"; 19 | default: 20 | return "application/octet-stream"; 21 | } 22 | } 23 | } 24 | 25 | export interface ResourceContext { 26 | requestorIpAddress?: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/resources/resourceClientFactory.ts: -------------------------------------------------------------------------------- 1 | import { ResourceClient } from "./resourceClient.js"; 2 | import { FilesystemResourceClient } from "./filesystemResourceClient.js"; 3 | import { GcsResourceClient } from "./gcsResourceClient.js"; 4 | import { GcsClient } from "../gcs/gcsClient.js"; 5 | 6 | let instance: ResourceClient | null = null; 7 | 8 | export type ResourceClientConfig = 9 | | { 10 | type: "filesystem"; 11 | imageStorageDirectory: string; 12 | } 13 | | { 14 | type: "gcs"; 15 | gcsConfig?: { 16 | privateKey?: string; 17 | clientEmail?: string; 18 | projectId?: string; 19 | bucketName?: string; 20 | }; 21 | }; 22 | 23 | export function initializeResourceClient(config: ResourceClientConfig) { 24 | if (instance) { 25 | throw new Error("ResourceClient has already been initialized"); 26 | } 27 | 28 | if (config.type === "filesystem") { 29 | instance = new FilesystemResourceClient(config.imageStorageDirectory); 30 | } else { 31 | const gcsClient = new GcsClient(config.gcsConfig); 32 | instance = new GcsResourceClient(gcsClient); 33 | } 34 | } 35 | 36 | export function getResourceClient(): ResourceClient { 37 | if (!instance) { 38 | throw new Error("ResourceClient has not been initialized"); 39 | } 40 | return instance; 41 | } 42 | -------------------------------------------------------------------------------- /src/sse.ts: -------------------------------------------------------------------------------- 1 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 2 | import express, { Request } from "express"; 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import bodyParser from "body-parser"; 5 | 6 | function getClientIp(req: Request): string { 7 | return ( 8 | // Check X-Forwarded-For header first (when behind a proxy/load balancer) 9 | req.get("x-forwarded-for")?.split(",")[0] || 10 | // Check X-Real-IP header (common with Nginx) 11 | req.get("x-real-ip") || 12 | // Check req.ip (Express built-in, respects trust proxy setting) 13 | req.ip || 14 | // Fallback to remoteAddress from the underlying socket 15 | req.socket.remoteAddress || 16 | // Final fallback 17 | "unknown" 18 | ); 19 | } 20 | 21 | export async function runSSEServer(server: Server) { 22 | let sseTransport: SSEServerTransport | null = null; 23 | const app = express(); 24 | 25 | // Used to allow parsing of the body of the request 26 | app.use("/*", bodyParser.json()); 27 | 28 | app.get("/sse", async (req, res) => { 29 | sseTransport = new SSEServerTransport("/messages", res); 30 | await server.connect(sseTransport); 31 | 32 | res.on("close", () => { 33 | sseTransport = null; 34 | }); 35 | }); 36 | 37 | app.post("/messages", async (req: Request, res) => { 38 | if (sseTransport) { 39 | // Parse the body and add the IP address 40 | const body = req.body; 41 | const params = req.body.params || {}; 42 | params._meta = { 43 | ip: getClientIp(req), 44 | headers: req.headers, 45 | }; 46 | const enrichedBody = { 47 | ...body, 48 | params, 49 | }; 50 | 51 | await sseTransport.handlePostMessage(req, res, enrichedBody); 52 | } else { 53 | res.status(400).send("No active SSE connection"); 54 | } 55 | }); 56 | 57 | // Handle 404s for all other routes 58 | app.use((req, res) => { 59 | res.status(404).json({ 60 | error: "Not Found", 61 | message: `Route ${req.method} ${req.path} not found`, 62 | timestamp: new Date().toISOString(), 63 | }); 64 | }); 65 | 66 | app.listen(3020, () => { 67 | console.error( 68 | "stability-ai MCP Server running on SSE at http://localhost:3020" 69 | ); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /src/stabilityAi/sd35Client.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | import FormData from "form-data"; 3 | import fs from "fs"; 4 | 5 | interface GenerateImageOptions { 6 | prompt: string; 7 | mode?: "text-to-image" | "image-to-image"; 8 | image?: string; // Path to image file for image-to-image 9 | strength?: number; // For image-to-image mode, range 0-1 10 | aspect_ratio?: "16:9" | "1:1" | "21:9" | "2:3" | "3:2" | "4:5" | "5:4" | "9:16" | "9:21"; 11 | model?: "sd3.5-large" | "sd3.5-large-turbo" | "sd3.5-medium" | "sd3-large" | "sd3-large-turbo" | "sd3-medium"; 12 | seed?: number; // Range 0-4294967294 13 | output_format?: "jpeg" | "png"; 14 | style_preset?: "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"; 15 | negative_prompt?: string; // Max 10000 characters 16 | cfg_scale?: number; // Range 1-10 17 | } 18 | 19 | export class SD35Client { 20 | private readonly apiKey: string; 21 | private readonly baseUrl = "https://api.stability.ai"; 22 | private readonly axiosClient: AxiosInstance; 23 | 24 | constructor(apiKey: string) { 25 | this.apiKey = apiKey; 26 | this.axiosClient = axios.create({ 27 | baseURL: this.baseUrl, 28 | timeout: 60000, 29 | maxBodyLength: Infinity, 30 | maxContentLength: Infinity, 31 | headers: { 32 | Authorization: `Bearer ${this.apiKey}`, 33 | Accept: "image/*", 34 | }, 35 | }); 36 | } 37 | 38 | async generateImage(options: GenerateImageOptions): Promise { 39 | const formData = new FormData(); 40 | 41 | // Required parameter 42 | if (!options.prompt || options.prompt.length === 0) { 43 | throw new Error("Prompt is required and cannot be empty"); 44 | } 45 | if (options.prompt.length > 10000) { 46 | throw new Error("Prompt cannot exceed 10000 characters"); 47 | } 48 | formData.append("prompt", options.prompt); 49 | 50 | // Set the mode (default to text-to-image) 51 | const mode = options.mode || "text-to-image"; 52 | formData.append("mode", mode); 53 | 54 | // Add specific parameters based on mode 55 | if (mode === "image-to-image") { 56 | if (!options.image) { 57 | throw new Error("Image path is required for image-to-image mode"); 58 | } 59 | formData.append("image", fs.createReadStream(options.image)); 60 | 61 | // Strength is required for image-to-image 62 | if (options.strength === undefined) { 63 | throw new Error("Strength parameter is required for image-to-image mode"); 64 | } 65 | if (options.strength < 0 || options.strength > 1) { 66 | throw new Error("Strength must be between 0 and 1"); 67 | } 68 | formData.append("strength", options.strength.toString()); 69 | } else { 70 | // aspect_ratio is only valid for text-to-image 71 | if (options.aspect_ratio) { 72 | formData.append("aspect_ratio", options.aspect_ratio); 73 | } 74 | } 75 | 76 | // Optional parameters 77 | if (options.model) { 78 | formData.append("model", options.model); 79 | } 80 | 81 | if (options.seed !== undefined) { 82 | if (options.seed < 0 || options.seed > 4294967294) { 83 | throw new Error("Seed must be between 0 and 4294967294"); 84 | } 85 | formData.append("seed", options.seed.toString()); 86 | } 87 | 88 | if (options.output_format) { 89 | formData.append("output_format", options.output_format); 90 | } 91 | 92 | if (options.style_preset) { 93 | formData.append("style_preset", options.style_preset); 94 | } 95 | 96 | if (options.negative_prompt) { 97 | if (options.negative_prompt.length > 10000) { 98 | throw new Error("Negative prompt cannot exceed 10000 characters"); 99 | } 100 | formData.append("negative_prompt", options.negative_prompt); 101 | } 102 | 103 | if (options.cfg_scale !== undefined) { 104 | if (options.cfg_scale < 1 || options.cfg_scale > 10) { 105 | throw new Error("CFG scale must be between 1 and 10"); 106 | } 107 | formData.append("cfg_scale", options.cfg_scale.toString()); 108 | } 109 | 110 | try { 111 | const response = await this.axiosClient.post( 112 | "/v2beta/stable-image/generate/sd3", 113 | formData, 114 | { 115 | headers: { 116 | ...formData.getHeaders(), 117 | }, 118 | responseType: "arraybuffer", 119 | } 120 | ); 121 | 122 | return response.data; 123 | } catch (error) { 124 | if (axios.isAxiosError(error) && error.response) { 125 | const data = error.response.data; 126 | if (error.response.status === 400) { 127 | let errorMessage = "Invalid parameters"; 128 | if (data.errors && Array.isArray(data.errors)) { 129 | errorMessage += `: ${data.errors.join(", ")}`; 130 | } 131 | throw new Error(errorMessage); 132 | } 133 | throw new Error(`API error (${error.response.status}): ${JSON.stringify(data)}`); 134 | } 135 | throw error; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/stabilityAi/stabilityAiApiClient.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | import FormData from "form-data"; 3 | import fs from "fs"; 4 | interface GenerateImageCoreOptions { 5 | aspectRatio?: 6 | | "16:9" 7 | | "1:1" 8 | | "21:9" 9 | | "2:3" 10 | | "3:2" 11 | | "4:5" 12 | | "5:4" 13 | | "9:16" 14 | | "9:21"; 15 | negativePrompt?: string; 16 | seed?: number; 17 | stylePreset?: 18 | | "3d-model" 19 | | "analog-film" 20 | | "anime" 21 | | "cinematic" 22 | | "comic-book" 23 | | "digital-art" 24 | | "enhance" 25 | | "fantasy-art" 26 | | "isometric" 27 | | "line-art" 28 | | "low-poly" 29 | | "modeling-compound" 30 | | "neon-punk" 31 | | "origami" 32 | | "photographic" 33 | | "pixel-art" 34 | | "tile-texture"; 35 | outputFormat?: "png" | "jpeg" | "webp"; 36 | } 37 | 38 | interface GenerateImageUltraOptions { 39 | aspectRatio?: 40 | | "16:9" 41 | | "1:1" 42 | | "21:9" 43 | | "2:3" 44 | | "3:2" 45 | | "4:5" 46 | | "5:4" 47 | | "9:16" 48 | | "9:21"; 49 | negativePrompt?: string; 50 | seed?: number; 51 | stylePreset?: 52 | | "3d-model" 53 | | "analog-film" 54 | | "anime" 55 | | "cinematic" 56 | | "comic-book" 57 | | "digital-art" 58 | | "enhance" 59 | | "fantasy-art" 60 | | "isometric" 61 | | "line-art" 62 | | "low-poly" 63 | | "modeling-compound" 64 | | "neon-punk" 65 | | "origami" 66 | | "photographic" 67 | | "pixel-art" 68 | | "tile-texture"; 69 | outputFormat?: "png" | "jpeg" | "webp"; 70 | } 71 | 72 | interface OutpaintOptions { 73 | left?: number; 74 | right?: number; 75 | up?: number; 76 | down?: number; 77 | creativity?: number; 78 | prompt?: string; 79 | seed?: number; 80 | outputFormat?: "png" | "jpeg" | "webp"; 81 | } 82 | 83 | interface SearchAndReplaceOptions { 84 | searchPrompt: string; 85 | prompt: string; 86 | } 87 | 88 | interface UpscaleCreativeOptions { 89 | prompt: string; 90 | negativePrompt?: string; 91 | seed?: number; 92 | outputFormat?: "png" | "jpeg" | "webp"; 93 | creativity?: number; 94 | } 95 | 96 | interface ControlSketchOptions { 97 | prompt: string; 98 | controlStrength?: number; 99 | negativePrompt?: string; 100 | seed?: number; 101 | outputFormat?: "png" | "jpeg" | "webp"; 102 | } 103 | 104 | interface SearchAndRecolorOptions { 105 | selectPrompt: string; 106 | prompt: string; 107 | growMask?: number; 108 | negativePrompt?: string; 109 | seed?: number; 110 | outputFormat?: "png" | "jpeg" | "webp"; 111 | } 112 | 113 | interface ReplaceBackgroundAndRelightOptions { 114 | backgroundPrompt?: string; 115 | backgroundReference?: string; 116 | foregroundPrompt?: string; 117 | negativePrompt?: string; 118 | preserveOriginalSubject?: number; 119 | originalBackgroundDepth?: number; 120 | keepOriginalBackground?: boolean; 121 | lightSourceDirection?: "above" | "below" | "left" | "right"; 122 | lightReference?: string; 123 | lightSourceStrength?: number; 124 | seed?: number; 125 | outputFormat?: "png" | "jpeg" | "webp"; 126 | } 127 | 128 | interface ControlStyleOptions { 129 | prompt: string; 130 | negativePrompt?: string; 131 | aspectRatio?: 132 | | "16:9" 133 | | "1:1" 134 | | "21:9" 135 | | "2:3" 136 | | "3:2" 137 | | "4:5" 138 | | "5:4" 139 | | "9:16" 140 | | "9:21"; 141 | fidelity?: number; 142 | seed?: number; 143 | outputFormat?: "png" | "jpeg" | "webp"; 144 | } 145 | 146 | interface ControlStructureOptions { 147 | prompt: string; 148 | controlStrength?: number; 149 | negativePrompt?: string; 150 | seed?: number; 151 | outputFormat?: "png" | "jpeg" | "webp"; 152 | } 153 | 154 | export class StabilityAiApiClient { 155 | private readonly apiKey: string; 156 | private readonly baseUrl = "https://api.stability.ai"; 157 | private readonly axiosClient: AxiosInstance; 158 | 159 | constructor(apiKey: string) { 160 | this.apiKey = apiKey; 161 | this.axiosClient = axios.create({ 162 | baseURL: this.baseUrl, 163 | timeout: 120000, 164 | maxBodyLength: Infinity, 165 | maxContentLength: Infinity, 166 | headers: { 167 | Authorization: `Bearer ${this.apiKey}`, 168 | Accept: "application/json", 169 | }, 170 | }); 171 | } 172 | 173 | // https://platform.stability.ai/docs/api-reference#tag/Generate/paths/~1v2beta~1stable-image~1generate~1core/post 174 | async generateImageCore( 175 | prompt: string, 176 | options?: GenerateImageCoreOptions 177 | ): Promise<{ base64Image: string }> { 178 | const payload = { 179 | prompt, 180 | output_format: "png", 181 | aspect_ratio: options?.aspectRatio, 182 | negative_prompt: options?.negativePrompt, 183 | style_preset: options?.stylePreset, 184 | ...options, 185 | }; 186 | 187 | return this.axiosClient 188 | .postForm( 189 | `${this.baseUrl}/v2beta/stable-image/generate/core`, 190 | axios.toFormData(payload, new FormData()) 191 | ) 192 | .then((res) => { 193 | const base64Image = res.data.image; 194 | return { 195 | base64Image, 196 | }; 197 | }); 198 | } 199 | 200 | async generateImageUltra( 201 | prompt: string, 202 | options?: GenerateImageUltraOptions 203 | ): Promise<{ base64Image: string }> { 204 | const payload = { 205 | prompt, 206 | output_format: options?.outputFormat || "png", 207 | ...options, 208 | }; 209 | 210 | return this.axiosClient 211 | .postForm( 212 | `${this.baseUrl}/v2beta/stable-image/generate/ultra`, 213 | axios.toFormData(payload, new FormData()) 214 | ) 215 | .then((res) => { 216 | const base64Image = res.data.image; 217 | return { 218 | base64Image, 219 | }; 220 | }); 221 | } 222 | 223 | async removeBackground( 224 | imageFilePath: string 225 | ): Promise<{ base64Image: string }> { 226 | const payload = { 227 | image: fs.createReadStream(imageFilePath), 228 | output_format: "png", 229 | }; 230 | 231 | try { 232 | const response = await this.axiosClient.postForm( 233 | `${this.baseUrl}/v2beta/stable-image/edit/remove-background`, 234 | axios.toFormData(payload, new FormData()) 235 | ); 236 | const base64Image = response.data.image; 237 | return { base64Image }; 238 | } catch (error) { 239 | if (axios.isAxiosError(error) && error.response) { 240 | const data = error.response.data; 241 | if (error.response.status === 400) { 242 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 243 | } 244 | throw new Error( 245 | `API error (${error.response.status}): ${JSON.stringify(data)}` 246 | ); 247 | } 248 | throw error; 249 | } 250 | } 251 | 252 | async outpaint( 253 | imageFilePath: string, 254 | options: OutpaintOptions 255 | ): Promise<{ base64Image: string }> { 256 | const payload = { 257 | image: fs.createReadStream(imageFilePath), 258 | output_format: options.outputFormat || "png", 259 | left: options.left || 0, 260 | right: options.right || 0, 261 | up: options.up || 0, 262 | down: options.down || 0, 263 | ...(options.creativity !== undefined && { 264 | creativity: options.creativity, 265 | }), 266 | ...(options.prompt && { prompt: options.prompt }), 267 | ...(options.seed && { seed: options.seed }), 268 | }; 269 | 270 | try { 271 | const response = await this.axiosClient.postForm( 272 | `${this.baseUrl}/v2beta/stable-image/edit/outpaint`, 273 | axios.toFormData(payload, new FormData()) 274 | ); 275 | const base64Image = response.data.image; 276 | return { base64Image }; 277 | } catch (error) { 278 | if (axios.isAxiosError(error) && error.response) { 279 | const data = error.response.data; 280 | if (error.response.status === 400) { 281 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 282 | } 283 | throw new Error( 284 | `API error (${error.response.status}): ${JSON.stringify(data)}` 285 | ); 286 | } 287 | throw error; 288 | } 289 | } 290 | 291 | async searchAndReplace( 292 | imageFilePath: string, 293 | options: SearchAndReplaceOptions 294 | ): Promise<{ base64Image: string }> { 295 | const payload = { 296 | image: fs.createReadStream(imageFilePath), 297 | output_format: "png", 298 | search_prompt: options.searchPrompt, 299 | prompt: options.prompt, 300 | }; 301 | 302 | try { 303 | const response = await this.axiosClient.postForm( 304 | `${this.baseUrl}/v2beta/stable-image/edit/search-and-replace`, 305 | axios.toFormData(payload, new FormData()) 306 | ); 307 | const base64Image = response.data.image; 308 | return { base64Image }; 309 | } catch (error) { 310 | if (axios.isAxiosError(error) && error.response) { 311 | const data = error.response.data; 312 | if (error.response.status === 400) { 313 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 314 | } 315 | throw new Error( 316 | `API error (${error.response.status}): ${JSON.stringify(data)}` 317 | ); 318 | } 319 | throw error; 320 | } 321 | } 322 | 323 | async upscaleFast(imageFilePath: string): Promise<{ base64Image: string }> { 324 | const payload = { 325 | image: fs.createReadStream(imageFilePath), 326 | output_format: "png", 327 | }; 328 | 329 | try { 330 | const response = await this.axiosClient.postForm( 331 | `${this.baseUrl}/v2beta/stable-image/upscale/fast`, 332 | axios.toFormData(payload, new FormData()) 333 | ); 334 | const base64Image = response.data.image; 335 | return { base64Image }; 336 | } catch (error: any) { 337 | if (error.response?.status === 400 && error.response?.data?.errors) { 338 | const errorMessage = `Invalid parameters: ${error.response.data.errors.join(", ")}`; 339 | throw new Error(errorMessage); 340 | } 341 | throw new Error( 342 | `API error (${error.response?.status}): ${JSON.stringify(error.response?.data)}` 343 | ); 344 | } 345 | } 346 | 347 | async fetchGenerationResult(id: string): Promise<{ base64Image: string }> { 348 | try { 349 | while (true) { 350 | const response = await this.axiosClient.get( 351 | `${this.baseUrl}/v2beta/results/${id}`, 352 | { 353 | headers: { 354 | Accept: "application/json", 355 | }, 356 | } 357 | ); 358 | 359 | if (response.status === 200) { 360 | return { base64Image: response.data.result }; 361 | } else if (response.status === 202) { 362 | // Generation still in progress, wait 10 seconds before polling again 363 | await new Promise((resolve) => setTimeout(resolve, 10000)); 364 | } else { 365 | throw new Error(`Unexpected status: ${response.status}`); 366 | } 367 | } 368 | } catch (error) { 369 | if (axios.isAxiosError(error) && error.response) { 370 | const data = error.response.data; 371 | if (error.response.status === 400) { 372 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 373 | } 374 | throw new Error( 375 | `API error (${error.response.status}): ${JSON.stringify(data)}` 376 | ); 377 | } 378 | throw error; 379 | } 380 | } 381 | 382 | async upscaleCreative( 383 | imageFilePath: string, 384 | options: UpscaleCreativeOptions 385 | ): Promise<{ base64Image: string }> { 386 | const payload = { 387 | image: fs.createReadStream(imageFilePath), 388 | prompt: options.prompt, 389 | output_format: options.outputFormat || "png", 390 | ...(options.negativePrompt && { 391 | negative_prompt: options.negativePrompt, 392 | }), 393 | ...(options.seed !== undefined && { seed: options.seed }), 394 | ...(options.creativity !== undefined && { 395 | creativity: options.creativity, 396 | }), 397 | }; 398 | 399 | try { 400 | const response = await this.axiosClient.postForm( 401 | `${this.baseUrl}/v2beta/stable-image/upscale/creative`, 402 | axios.toFormData(payload, new FormData()) 403 | ); 404 | 405 | // Get the generation ID from the response 406 | const generationId = response.data.id; 407 | 408 | // Poll for the result 409 | return await this.fetchGenerationResult(generationId); 410 | } catch (error) { 411 | if (axios.isAxiosError(error) && error.response) { 412 | const data = error.response.data; 413 | if (error.response.status === 400) { 414 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 415 | } 416 | throw new Error( 417 | `API error (${error.response.status}): ${JSON.stringify(data)}` 418 | ); 419 | } 420 | throw error; 421 | } 422 | } 423 | 424 | async controlSketch( 425 | imageFilePath: string, 426 | options: ControlSketchOptions 427 | ): Promise<{ base64Image: string }> { 428 | const payload = { 429 | image: fs.createReadStream(imageFilePath), 430 | prompt: options.prompt, 431 | output_format: options.outputFormat || "png", 432 | ...(options.controlStrength !== undefined && { 433 | control_strength: options.controlStrength, 434 | }), 435 | ...(options.negativePrompt && { 436 | negative_prompt: options.negativePrompt, 437 | }), 438 | ...(options.seed !== undefined && { seed: options.seed }), 439 | }; 440 | 441 | try { 442 | const response = await this.axiosClient.postForm( 443 | `${this.baseUrl}/v2beta/stable-image/control/sketch`, 444 | axios.toFormData(payload, new FormData()) 445 | ); 446 | const base64Image = response.data.image; 447 | return { base64Image }; 448 | } catch (error) { 449 | if (axios.isAxiosError(error) && error.response) { 450 | const data = error.response.data; 451 | if (error.response.status === 400) { 452 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 453 | } 454 | throw new Error( 455 | `API error (${error.response.status}): ${JSON.stringify(data)}` 456 | ); 457 | } 458 | throw error; 459 | } 460 | } 461 | 462 | async searchAndRecolor( 463 | imageFilePath: string, 464 | options: SearchAndRecolorOptions 465 | ): Promise<{ base64Image: string }> { 466 | const payload = { 467 | image: fs.createReadStream(imageFilePath), 468 | prompt: options.prompt, 469 | select_prompt: options.selectPrompt, 470 | output_format: options.outputFormat || "png", 471 | ...(options.growMask !== undefined && { grow_mask: options.growMask }), 472 | ...(options.negativePrompt && { 473 | negative_prompt: options.negativePrompt, 474 | }), 475 | ...(options.seed !== undefined && { seed: options.seed }), 476 | }; 477 | 478 | try { 479 | const response = await this.axiosClient.postForm( 480 | `${this.baseUrl}/v2beta/stable-image/edit/search-and-recolor`, 481 | axios.toFormData(payload, new FormData()) 482 | ); 483 | const base64Image = response.data.image; 484 | return { base64Image }; 485 | } catch (error) { 486 | if (axios.isAxiosError(error) && error.response) { 487 | const data = error.response.data; 488 | if (error.response.status === 400) { 489 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 490 | } 491 | throw new Error( 492 | `API error (${error.response.status}): ${JSON.stringify(data)}` 493 | ); 494 | } 495 | throw error; 496 | } 497 | } 498 | 499 | async replaceBackgroundAndRelight( 500 | imageFilePath: string, 501 | options: ReplaceBackgroundAndRelightOptions 502 | ): Promise<{ base64Image: string }> { 503 | const payload = { 504 | subject_image: fs.createReadStream(imageFilePath), 505 | output_format: options.outputFormat || "png", 506 | ...(options.backgroundPrompt && { 507 | background_prompt: options.backgroundPrompt, 508 | }), 509 | ...(options.backgroundReference && { 510 | background_reference: fs.createReadStream(options.backgroundReference), 511 | }), 512 | ...(options.foregroundPrompt && { 513 | foreground_prompt: options.foregroundPrompt, 514 | }), 515 | ...(options.negativePrompt && { 516 | negative_prompt: options.negativePrompt, 517 | }), 518 | ...(options.preserveOriginalSubject !== undefined && { 519 | preserve_original_subject: options.preserveOriginalSubject, 520 | }), 521 | ...(options.originalBackgroundDepth !== undefined && { 522 | original_background_depth: options.originalBackgroundDepth, 523 | }), 524 | ...(options.keepOriginalBackground !== undefined && { 525 | keep_original_background: options.keepOriginalBackground, 526 | }), 527 | ...(options.lightSourceDirection && { 528 | light_source_direction: options.lightSourceDirection, 529 | }), 530 | ...(options.lightReference && { 531 | light_reference: fs.createReadStream(options.lightReference), 532 | }), 533 | ...(options.lightSourceStrength !== undefined && { 534 | light_source_strength: options.lightSourceStrength, 535 | }), 536 | ...(options.seed !== undefined && { seed: options.seed }), 537 | }; 538 | 539 | try { 540 | const response = await this.axiosClient.postForm( 541 | `${this.baseUrl}/v2beta/stable-image/edit/replace-background-and-relight`, 542 | axios.toFormData(payload, new FormData()) 543 | ); 544 | 545 | // Get the generation ID from the response 546 | const generationId = response.data.id; 547 | 548 | // Poll for the result 549 | return await this.fetchGenerationResult(generationId); 550 | } catch (error) { 551 | if (axios.isAxiosError(error) && error.response) { 552 | const data = error.response.data; 553 | if (error.response.status === 400) { 554 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 555 | } 556 | throw new Error( 557 | `API error (${error.response.status}): ${JSON.stringify(data)}` 558 | ); 559 | } 560 | throw error; 561 | } 562 | } 563 | 564 | async controlStyle( 565 | imageFilePath: string, 566 | options: ControlStyleOptions 567 | ): Promise<{ base64Image: string }> { 568 | const payload = { 569 | image: fs.createReadStream(imageFilePath), 570 | prompt: options.prompt, 571 | output_format: options.outputFormat || "png", 572 | ...(options.negativePrompt && { 573 | negative_prompt: options.negativePrompt, 574 | }), 575 | ...(options.aspectRatio && { 576 | aspect_ratio: options.aspectRatio, 577 | }), 578 | ...(options.fidelity !== undefined && { 579 | fidelity: options.fidelity, 580 | }), 581 | ...(options.seed !== undefined && { seed: options.seed }), 582 | }; 583 | 584 | try { 585 | const response = await this.axiosClient.postForm( 586 | `${this.baseUrl}/v2beta/stable-image/control/style`, 587 | axios.toFormData(payload, new FormData()) 588 | ); 589 | const base64Image = response.data.image; 590 | return { base64Image }; 591 | } catch (error) { 592 | if (axios.isAxiosError(error) && error.response) { 593 | const data = error.response.data; 594 | if (error.response.status === 400) { 595 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 596 | } 597 | throw new Error( 598 | `API error (${error.response.status}): ${JSON.stringify(data)}` 599 | ); 600 | } 601 | throw error; 602 | } 603 | } 604 | 605 | async controlStructure( 606 | imageFilePath: string, 607 | options: ControlStructureOptions 608 | ): Promise<{ base64Image: string }> { 609 | const payload = { 610 | image: fs.createReadStream(imageFilePath), 611 | prompt: options.prompt, 612 | output_format: options.outputFormat || "png", 613 | ...(options.controlStrength !== undefined && { 614 | control_strength: options.controlStrength, 615 | }), 616 | ...(options.negativePrompt && { 617 | negative_prompt: options.negativePrompt, 618 | }), 619 | ...(options.seed !== undefined && { seed: options.seed }), 620 | }; 621 | 622 | try { 623 | const response = await this.axiosClient.postForm( 624 | `${this.baseUrl}/v2beta/stable-image/control/structure`, 625 | axios.toFormData(payload, new FormData()) 626 | ); 627 | const base64Image = response.data.image; 628 | return { base64Image }; 629 | } catch (error) { 630 | if (axios.isAxiosError(error) && error.response) { 631 | const data = error.response.data; 632 | if (error.response.status === 400) { 633 | throw new Error(`Invalid parameters: ${data.errors.join(", ")}`); 634 | } 635 | throw new Error( 636 | `API error (${error.response.status}): ${JSON.stringify(data)}` 637 | ); 638 | } 639 | throw error; 640 | } 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /src/stdio.ts: -------------------------------------------------------------------------------- 1 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | 4 | export async function runStdioServer(server: Server) { 5 | const transport = new StdioServerTransport(); 6 | await server.connect(transport); 7 | console.error("stability-ai MCP Server running on stdio"); 8 | } 9 | -------------------------------------------------------------------------------- /src/tools/controlSketch.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import open from "open"; 3 | import { z } from "zod"; 4 | import { ResourceContext } from "../resources/resourceClient.js"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const ControlSketchArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | prompt: z.string(), 10 | controlStrength: z.number().min(0).max(1).optional(), 11 | negativePrompt: z.string().optional(), 12 | outputImageFileName: z.string(), 13 | }); 14 | 15 | export type ControlSketchArgs = z.infer; 16 | 17 | export const controlSketchToolDefinition = { 18 | name: "stability-ai-control-sketch", 19 | description: `Translate hand-drawn sketches to production-grade images.`, 20 | inputSchema: { 21 | type: "object", 22 | properties: { 23 | imageFileUri: { 24 | type: "string", 25 | description: `The URI to the image file. It should start with file://`, 26 | }, 27 | prompt: { 28 | type: "string", 29 | description: 30 | "What you wish to see in the output image. A strong, descriptive prompt that clearly defines elements, colors, and subjects will lead to better results.\n\nTo control the weight of a given word use the format (word:weight), where word is the word you'd like to control the weight of and weight is a value between 0 and 1. For example: The sky was a crisp (blue:0.3) and (green:0.8) would convey a sky that was blue and green, but more green than blue.", 31 | }, 32 | controlStrength: { 33 | type: "number", 34 | description: 35 | "How much influence, or control, the image has on the generation. Represented as a float between 0 and 1, where 0 is the least influence and 1 is the maximum.", 36 | minimum: 0, 37 | maximum: 1, 38 | }, 39 | negativePrompt: { 40 | type: "string", 41 | description: "What you do not wish to see in the output image.", 42 | }, 43 | outputImageFileName: { 44 | type: "string", 45 | description: 46 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 47 | }, 48 | }, 49 | required: ["imageFileUri", "prompt", "outputImageFileName"], 50 | }, 51 | }; 52 | 53 | export async function controlSketch( 54 | args: ControlSketchArgs, 55 | context: ResourceContext 56 | ) { 57 | const validatedArgs = ControlSketchArgsSchema.parse(args); 58 | 59 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 60 | 61 | const resourceClient = getResourceClient(); 62 | const imageFilePath = await resourceClient.resourceToFile( 63 | validatedArgs.imageFileUri, 64 | context 65 | ); 66 | 67 | try { 68 | const response = await client.controlSketch(imageFilePath, { 69 | prompt: validatedArgs.prompt, 70 | controlStrength: validatedArgs.controlStrength, 71 | negativePrompt: validatedArgs.negativePrompt, 72 | }); 73 | 74 | const imageAsBase64 = response.base64Image; 75 | const filename = `${validatedArgs.outputImageFileName}.png`; 76 | 77 | const resource = await resourceClient.createResource( 78 | filename, 79 | imageAsBase64 80 | ); 81 | 82 | if (resource.uri.includes("file://")) { 83 | const file_location = resource.uri.replace("file://", ""); 84 | open(file_location); 85 | } 86 | 87 | return { 88 | content: [ 89 | { 90 | type: "text", 91 | text: `Processed sketch "${validatedArgs.imageFileUri}" with prompt "${validatedArgs.prompt}" to create the following image:`, 92 | }, 93 | { 94 | type: "resource", 95 | resource: resource, 96 | }, 97 | ], 98 | }; 99 | } catch (error) { 100 | const errorMessage = 101 | error instanceof Error ? error.message : "Unknown error"; 102 | return { 103 | content: [ 104 | { 105 | type: "text", 106 | text: `Failed to process sketch: ${errorMessage}`, 107 | }, 108 | ], 109 | }; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/tools/controlStructure.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import open from "open"; 4 | import { z } from "zod"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const ControlStructureArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | prompt: z.string().min(1).max(10000), 10 | controlStrength: z.number().min(0).max(1).optional(), 11 | negativePrompt: z.string().max(10000).optional(), 12 | outputImageFileName: z.string(), 13 | }); 14 | 15 | export type ControlStructureArgs = z.infer; 16 | 17 | export const controlStructureToolDefinition = { 18 | name: "stability-ai-control-structure", 19 | description: 20 | "Generate a new image while maintaining the structure of a reference image", 21 | inputSchema: { 22 | type: "object", 23 | properties: { 24 | imageFileUri: { 25 | type: "string", 26 | description: 27 | "The URI to the structure reference image file. It should start with file://", 28 | }, 29 | prompt: { 30 | type: "string", 31 | description: "What you wish to see in the output image", 32 | }, 33 | controlStrength: { 34 | type: "number", 35 | description: 36 | "How much influence the reference image has on the generation (0-1)", 37 | }, 38 | negativePrompt: { 39 | type: "string", 40 | description: "Optional description of what you don't want to see", 41 | }, 42 | outputImageFileName: { 43 | type: "string", 44 | description: 45 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 46 | }, 47 | }, 48 | required: ["imageFileUri", "prompt", "outputImageFileName"], 49 | }, 50 | }; 51 | 52 | export const controlStructure = async ( 53 | args: ControlStructureArgs, 54 | context: ResourceContext 55 | ) => { 56 | const validatedArgs = ControlStructureArgsSchema.parse(args); 57 | 58 | const resourceClient = getResourceClient(); 59 | const imageFilePath = await resourceClient.resourceToFile( 60 | validatedArgs.imageFileUri, 61 | context 62 | ); 63 | 64 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 65 | 66 | const response = await client.controlStructure(imageFilePath, { 67 | prompt: validatedArgs.prompt, 68 | controlStrength: validatedArgs.controlStrength, 69 | negativePrompt: validatedArgs.negativePrompt, 70 | }); 71 | 72 | const imageAsBase64 = response.base64Image; 73 | const filename = `${validatedArgs.outputImageFileName}.png`; 74 | 75 | const resource = await resourceClient.createResource(filename, imageAsBase64); 76 | 77 | if (resource.uri.includes("file://")) { 78 | const file_location = resource.uri.replace("file://", ""); 79 | open(file_location); 80 | } 81 | 82 | return { 83 | content: [ 84 | { 85 | type: "text", 86 | text: `Generated image "${validatedArgs.outputImageFileName}" using the structure of "${validatedArgs.imageFileUri}"`, 87 | }, 88 | { 89 | type: "resource", 90 | resource: resource, 91 | }, 92 | ], 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /src/tools/controlStyle.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import open from "open"; 4 | import { z } from "zod"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const ControlStyleArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | prompt: z.string().min(1).max(10000), 10 | negativePrompt: z.string().max(10000).optional(), 11 | aspectRatio: z 12 | .enum(["16:9", "1:1", "21:9", "2:3", "3:2", "4:5", "5:4", "9:16", "9:21"]) 13 | .optional(), 14 | fidelity: z.number().min(0).max(1).optional(), 15 | outputImageFileName: z.string(), 16 | }); 17 | 18 | export type ControlStyleArgs = z.infer; 19 | 20 | export const controlStyleToolDefinition = { 21 | name: "stability-ai-control-style", 22 | description: "Generate a new image in the style of a reference image", 23 | inputSchema: { 24 | type: "object", 25 | properties: { 26 | imageFileUri: { 27 | type: "string", 28 | description: 29 | "The URI to the style reference image file. It should start with file://", 30 | }, 31 | prompt: { 32 | type: "string", 33 | description: "What you wish to see in the output image", 34 | }, 35 | negativePrompt: { 36 | type: "string", 37 | description: "Optional description of what you don't want to see", 38 | }, 39 | aspectRatio: { 40 | type: "string", 41 | enum: [ 42 | "16:9", 43 | "1:1", 44 | "21:9", 45 | "2:3", 46 | "3:2", 47 | "4:5", 48 | "5:4", 49 | "9:16", 50 | "9:21", 51 | ], 52 | description: "Optional aspect ratio for the generated image", 53 | }, 54 | fidelity: { 55 | type: "number", 56 | description: 57 | "How closely the output image's style should match the input (0-1)", 58 | }, 59 | outputImageFileName: { 60 | type: "string", 61 | description: 62 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 63 | }, 64 | }, 65 | required: ["imageFileUri", "prompt", "outputImageFileName"], 66 | }, 67 | }; 68 | 69 | export const controlStyle = async ( 70 | args: ControlStyleArgs, 71 | context: ResourceContext 72 | ) => { 73 | const validatedArgs = ControlStyleArgsSchema.parse(args); 74 | 75 | const resourceClient = getResourceClient(); 76 | const imageFilePath = await resourceClient.resourceToFile( 77 | validatedArgs.imageFileUri, 78 | context 79 | ); 80 | 81 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 82 | 83 | const response = await client.controlStyle(imageFilePath, { 84 | prompt: validatedArgs.prompt, 85 | negativePrompt: validatedArgs.negativePrompt, 86 | aspectRatio: validatedArgs.aspectRatio, 87 | fidelity: validatedArgs.fidelity, 88 | }); 89 | 90 | const imageAsBase64 = response.base64Image; 91 | const filename = `${validatedArgs.outputImageFileName}.png`; 92 | 93 | const resource = await resourceClient.createResource(filename, imageAsBase64); 94 | 95 | if (resource.uri.includes("file://")) { 96 | const file_location = resource.uri.replace("file://", ""); 97 | open(file_location); 98 | } 99 | 100 | return { 101 | content: [ 102 | { 103 | type: "text", 104 | text: `Generated image "${validatedArgs.outputImageFileName}" in the style of "${validatedArgs.imageFileUri}"`, 105 | }, 106 | { 107 | type: "resource", 108 | resource: resource, 109 | }, 110 | ], 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /src/tools/generateImageCore.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import open from "open"; 3 | import { z } from "zod"; 4 | import { ResourceContext } from "../resources/resourceClient.js"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | import { saveMetadata } from "../utils/metadataUtils.js"; 7 | 8 | // Constants for shared values 9 | const ASPECT_RATIOS = [ 10 | "16:9", 11 | "1:1", 12 | "21:9", 13 | "2:3", 14 | "3:2", 15 | "4:5", 16 | "5:4", 17 | "9:16", 18 | "9:21", 19 | ] as const; 20 | 21 | const STYLE_PRESETS = [ 22 | "3d-model", 23 | "analog-film", 24 | "anime", 25 | "cinematic", 26 | "comic-book", 27 | "digital-art", 28 | "enhance", 29 | "fantasy-art", 30 | "isometric", 31 | "line-art", 32 | "low-poly", 33 | "modeling-compound", 34 | "neon-punk", 35 | "origami", 36 | "photographic", 37 | "pixel-art", 38 | "tile-texture", 39 | ] as const; 40 | 41 | const DESCRIPTIONS = { 42 | prompt: 43 | "What you wish to see in the output image. A strong, descriptive prompt that clearly defines elements, colors, and subjects will lead to better results.\n\nTo control the weight of a given word use the format (word:weight), where word is the word you'd like to control the weight of and weight is a value between 0 and 1. For example: The sky was a crisp (blue:0.3) and (green:0.8) would convey a sky that was blue and green, but more green than blue.", 44 | aspectRatio: "Controls the aspect ratio of the generated image.", 45 | negativePrompt: 46 | "A blurb of text describing what you do not wish to see in the output image. This is an advanced feature. If your user does not give specific guidance for a negative prompt, fill in some sensible defaults based on how descriptive the user is about their intended image", 47 | stylePreset: "Guides the image model towards a particular style.", 48 | outputImageFileName: 49 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 50 | } as const; 51 | 52 | // Zod schema 53 | const GenerateImageCoreArgsSchema = z.object({ 54 | prompt: z.string().min(1, "Prompt cannot be empty").max(10000), 55 | aspectRatio: z.enum(ASPECT_RATIOS).optional().default("1:1"), 56 | negativePrompt: z.string().max(10000).optional(), 57 | stylePreset: z.enum(STYLE_PRESETS).optional(), 58 | outputImageFileName: z.string(), 59 | }); 60 | 61 | export type generateImageCoreArgs = z.infer; 62 | // Alias for backwards compatibility 63 | export type GenerateImageCoreArgs = generateImageCoreArgs; 64 | 65 | // Tool definition using the same constants 66 | export const generateImageCoreToolDefinition = { 67 | name: "stability-ai-generate-image-core", 68 | description: "Generate an image using Stability AI's Core API service based on a provided prompt. This model and endpoint is a sensible default to use when not prompted to use a specific one, offering a good balance of cost and quality", 69 | inputSchema: { 70 | type: "object", 71 | properties: { 72 | prompt: { 73 | type: "string", 74 | description: DESCRIPTIONS.prompt, 75 | minLength: 1, 76 | maxLength: 10000, 77 | }, 78 | aspectRatio: { 79 | type: "string", 80 | enum: ASPECT_RATIOS, 81 | description: DESCRIPTIONS.aspectRatio, 82 | default: "1:1", 83 | }, 84 | negativePrompt: { 85 | type: "string", 86 | description: DESCRIPTIONS.negativePrompt, 87 | maxLength: 10000, 88 | }, 89 | stylePreset: { 90 | type: "string", 91 | enum: STYLE_PRESETS, 92 | description: DESCRIPTIONS.stylePreset, 93 | }, 94 | outputImageFileName: { 95 | type: "string", 96 | description: DESCRIPTIONS.outputImageFileName, 97 | }, 98 | }, 99 | required: ["prompt", "outputImageFileName"], 100 | }, 101 | } as const; 102 | 103 | // Tool definition is now defined solely in generateImage.ts for backward compatibility 104 | 105 | export const generateImageCore = async ( 106 | args: GenerateImageCoreArgs, 107 | context: ResourceContext 108 | ) => { 109 | const { 110 | prompt, 111 | aspectRatio, 112 | negativePrompt, 113 | stylePreset, 114 | outputImageFileName, 115 | } = GenerateImageCoreArgsSchema.parse(args); 116 | 117 | // Capture request parameters for metadata 118 | const requestParams = { 119 | prompt, 120 | aspectRatio, 121 | negativePrompt, 122 | stylePreset, 123 | model: "core", 124 | outputImageFileName, 125 | }; 126 | 127 | try { 128 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY); 129 | const response = await client.generateImageCore(prompt, { 130 | aspectRatio, 131 | negativePrompt, 132 | stylePreset, 133 | }); 134 | 135 | const imageAsBase64 = response.base64Image; 136 | const filename = `${outputImageFileName}.png`; 137 | 138 | const resourceClient = getResourceClient(); 139 | const resource = await resourceClient.createResource( 140 | filename, 141 | imageAsBase64, 142 | context 143 | ); 144 | 145 | if (resource.uri.includes("file://")) { 146 | const file_location = resource.uri.replace("file://", ""); 147 | 148 | // Save metadata to a text file 149 | saveMetadata(file_location, requestParams, { 150 | responseType: "success", 151 | timeGenerated: new Date().toISOString() 152 | }); 153 | 154 | open(file_location); 155 | } 156 | 157 | return { 158 | content: [ 159 | { 160 | type: "text", 161 | text: `Processed \`${prompt}\` with Stability Core to create the following image:`, 162 | }, 163 | { 164 | type: "resource", 165 | resource: resource, 166 | }, 167 | ], 168 | }; 169 | } catch (error) { 170 | // Handle errors and save error metadata if enabled 171 | if (process.env.SAVE_METADATA_FAILED === 'true') { 172 | // Create a temp path for the failed request metadata 173 | const errorFilePath = `${process.env.IMAGE_STORAGE_DIRECTORY}/${outputImageFileName}-failed-${Date.now()}.txt`; 174 | saveMetadata(errorFilePath, requestParams, undefined, error as Error | string); 175 | } 176 | throw error; 177 | } 178 | }; 179 | 180 | // The original function is now defined solely in generateImage.ts for backward compatibility 181 | -------------------------------------------------------------------------------- /src/tools/generateImageSD35.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 4 | import { SD35Client } from "../stabilityAi/sd35Client.js"; 5 | import open from "open"; 6 | import { saveMetadata } from "../utils/metadataUtils.js"; 7 | 8 | // Constants for shared values 9 | const ASPECT_RATIOS = [ 10 | "16:9", 11 | "1:1", 12 | "21:9", 13 | "2:3", 14 | "3:2", 15 | "4:5", 16 | "5:4", 17 | "9:16", 18 | "9:21" 19 | ] as const; 20 | 21 | const STYLE_PRESETS = [ 22 | "3d-model", 23 | "analog-film", 24 | "anime", 25 | "cinematic", 26 | "comic-book", 27 | "digital-art", 28 | "enhance", 29 | "fantasy-art", 30 | "isometric", 31 | "line-art", 32 | "low-poly", 33 | "modeling-compound", 34 | "neon-punk", 35 | "origami", 36 | "photographic", 37 | "pixel-art", 38 | "tile-texture" 39 | ] as const; 40 | 41 | const MODELS = [ 42 | "sd3.5-large", 43 | "sd3.5-large-turbo", 44 | "sd3.5-medium", 45 | "sd3-large", 46 | "sd3-large-turbo", 47 | "sd3-medium" 48 | ] as const; 49 | 50 | // Zod schema 51 | const GenerateImageSD35ArgsSchema = z.object({ 52 | prompt: z.string().min(1, "Prompt cannot be empty").max(10000), 53 | aspectRatio: z.enum(ASPECT_RATIOS).optional().default("1:1"), 54 | negativePrompt: z.string().max(10000).optional(), 55 | stylePreset: z.enum(STYLE_PRESETS).optional(), 56 | cfgScale: z.number().min(1).max(10).optional(), 57 | seed: z.number().min(0).max(4294967294).optional(), 58 | model: z.enum(MODELS).optional().default("sd3.5-large"), 59 | outputFormat: z.enum(["jpeg", "png"]).optional().default("png"), 60 | outputImageFileName: z.string() 61 | }); 62 | 63 | export type GenerateImageSD35Args = z.infer; 64 | 65 | // Tool definition 66 | export const generateImageSD35ToolDefinition = { 67 | name: "stability-ai-generate-image-sd35", 68 | description: "Generate an image using Stable Diffusion 3.5 models with advanced configuration options.", 69 | inputSchema: { 70 | type: "object", 71 | properties: { 72 | prompt: { 73 | type: "string", 74 | description: "What you wish to see in the output image. A strong, descriptive prompt that clearly defines elements, colors, and subjects will lead to better results.", 75 | minLength: 1, 76 | maxLength: 10000 77 | }, 78 | aspectRatio: { 79 | type: "string", 80 | enum: ASPECT_RATIOS, 81 | description: "Controls the aspect ratio of the generated image.", 82 | default: "1:1" 83 | }, 84 | negativePrompt: { 85 | type: "string", 86 | description: "Keywords of what you do not wish to see in the output image. This helps avoid unwanted elements. Maximum 10000 characters. If your user does not give specific guidance for a negative prompt, fill in some sensible defaults based on how descriptive the user is about their intended image", 87 | maxLength: 10000 88 | }, 89 | stylePreset: { 90 | type: "string", 91 | enum: STYLE_PRESETS, 92 | description: "Guides the image model towards a particular style." 93 | }, 94 | cfgScale: { 95 | type: "number", 96 | minimum: 1, 97 | maximum: 10, 98 | description: "How strictly the diffusion process adheres to the prompt text. Values range from 1-10, with higher values keeping your image closer to your prompt." 99 | }, 100 | seed: { 101 | type: "number", 102 | minimum: 0, 103 | maximum: 4294967294, 104 | description: "A specific value that guides the 'randomness' of the generation. (Omit or use 0 for random seed)" 105 | }, 106 | model: { 107 | type: "string", 108 | enum: MODELS, 109 | description: "The model to use for generation: SD3.5 Large (8B params, high quality), Medium (2.5B params, balanced), or Turbo (faster) variants. SD3.5 costs range from 3.5-6.5 credits per generation.", 110 | default: "sd3.5-large" 111 | }, 112 | outputFormat: { 113 | type: "string", 114 | enum: ["jpeg", "png"], 115 | description: "The format of the output image.", 116 | default: "png" 117 | }, 118 | outputImageFileName: { 119 | type: "string", 120 | description: "The desired name of the output image file, no file extension." 121 | } 122 | }, 123 | required: ["prompt", "outputImageFileName"] 124 | } 125 | } as const; 126 | 127 | // Implementation 128 | export const generateImageSD35 = async ( 129 | args: GenerateImageSD35Args, 130 | context: ResourceContext 131 | ) => { 132 | const { 133 | prompt, 134 | aspectRatio, 135 | negativePrompt, 136 | stylePreset, 137 | cfgScale, 138 | seed, 139 | model, 140 | outputFormat, 141 | outputImageFileName 142 | } = GenerateImageSD35ArgsSchema.parse(args); 143 | 144 | // Capture request parameters for metadata 145 | const requestParams = { 146 | prompt, 147 | aspectRatio, 148 | negativePrompt, 149 | stylePreset, 150 | cfgScale, 151 | seed, 152 | model, 153 | outputFormat, 154 | outputImageFileName 155 | }; 156 | 157 | try { 158 | const client = new SD35Client(process.env.STABILITY_AI_API_KEY); 159 | 160 | // Convert to SD35Client format 161 | const imageBuffer = await client.generateImage({ 162 | prompt, 163 | aspect_ratio: aspectRatio, 164 | negative_prompt: negativePrompt, 165 | style_preset: stylePreset, 166 | cfg_scale: cfgScale, 167 | seed, 168 | model, 169 | output_format: outputFormat, 170 | mode: "text-to-image" 171 | }); 172 | 173 | // Convert buffer to base64 174 | const imageAsBase64 = imageBuffer.toString('base64'); 175 | const filename = `${outputImageFileName}.${outputFormat}`; 176 | 177 | const resourceClient = getResourceClient(); 178 | const resource = await resourceClient.createResource( 179 | filename, 180 | imageAsBase64, 181 | context 182 | ); 183 | 184 | if (resource.uri.includes("file://")) { 185 | const file_location = resource.uri.replace("file://", ""); 186 | 187 | // Save metadata to a text file 188 | saveMetadata(file_location, requestParams, { 189 | responseType: "success", 190 | timeGenerated: new Date().toISOString() 191 | }); 192 | 193 | open(file_location); 194 | } 195 | 196 | return { 197 | content: [ 198 | { 199 | type: "text", 200 | text: `Processed \`${prompt}\` with ${model} to create the following image:`, 201 | }, 202 | { 203 | type: "resource", 204 | resource: resource, 205 | }, 206 | ], 207 | }; 208 | } catch (error) { 209 | // Handle errors and save error metadata if enabled 210 | if (process.env.SAVE_METADATA_FAILED === 'true') { 211 | // Create a temp path for the failed request metadata 212 | const errorFilePath = `${process.env.IMAGE_STORAGE_DIRECTORY}/${outputImageFileName}-failed-${Date.now()}.txt`; 213 | saveMetadata(errorFilePath, requestParams, undefined, error as Error | string); 214 | } 215 | throw error; 216 | } 217 | }; 218 | -------------------------------------------------------------------------------- /src/tools/generateImageUltra.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 4 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 5 | import open from "open"; 6 | import { saveMetadata } from "../utils/metadataUtils.js"; 7 | 8 | // Constants for shared values 9 | const ASPECT_RATIOS = [ 10 | "16:9", 11 | "1:1", 12 | "21:9", 13 | "2:3", 14 | "3:2", 15 | "4:5", 16 | "5:4", 17 | "9:16", 18 | "9:21" 19 | ] as const; 20 | 21 | const STYLE_PRESETS = [ 22 | "3d-model", 23 | "analog-film", 24 | "anime", 25 | "cinematic", 26 | "comic-book", 27 | "digital-art", 28 | "enhance", 29 | "fantasy-art", 30 | "isometric", 31 | "line-art", 32 | "low-poly", 33 | "modeling-compound", 34 | "neon-punk", 35 | "origami", 36 | "photographic", 37 | "pixel-art", 38 | "tile-texture" 39 | ] as const; 40 | 41 | const OUTPUT_FORMATS = ["jpeg", "png", "webp"] as const; 42 | 43 | // Zod schema 44 | const GenerateImageUltraArgsSchema = z.object({ 45 | prompt: z.string().min(1, "Prompt cannot be empty").max(10000), 46 | aspectRatio: z.enum(ASPECT_RATIOS).optional().default("1:1"), 47 | negativePrompt: z.string().max(10000).optional(), 48 | stylePreset: z.enum(STYLE_PRESETS).optional(), 49 | seed: z.number().min(0).max(4294967294).optional(), 50 | outputFormat: z.enum(OUTPUT_FORMATS).optional().default("png"), 51 | outputImageFileName: z.string() 52 | }); 53 | 54 | export type GenerateImageUltraArgs = z.infer; 55 | 56 | // Tool definition 57 | export const generateImageUltraToolDefinition = { 58 | name: "stability-ai-generate-image-ultra", 59 | description: "Generate an image using Stability AI's most advanced Ultra service, offering high quality images with unprecedented prompt understanding, excellent typography, complex compositions, and dynamic lighting. Note that Ultra is significantly expensive than Core models, and should not be a default option when prompted to generate an image unless specifically instructed to use Ultra for a session in advance." 60 | inputSchema: { 61 | type: "object", 62 | properties: { 63 | prompt: { 64 | type: "string", 65 | description: "What you wish to see in the output image. A strong, descriptive prompt that clearly defines elements, colors, and subjects will lead to better results. To control the weight of a given word use the format (word:weight), where word is the word you'd like to control the weight of and weight is a value between 0 and 1", 66 | minLength: 1, 67 | maxLength: 10000 68 | }, 69 | aspectRatio: { 70 | type: "string", 71 | enum: ASPECT_RATIOS, 72 | description: "Controls the aspect ratio of the generated image.", 73 | default: "1:1" 74 | }, 75 | negativePrompt: { 76 | type: "string", 77 | description: "Keywords of what you do not wish to see in the output image. This helps avoid unwanted elements. Maximum 10000 characters. If your user does not give specific guidance for a negative prompt, fill in some sensible defaults based on how descriptive the user is about their intended image", 78 | maxLength: 10000 79 | }, 80 | stylePreset: { 81 | type: "string", 82 | enum: STYLE_PRESETS, 83 | description: "Guides the image model towards a particular style." 84 | }, 85 | seed: { 86 | type: "number", 87 | minimum: 0, 88 | maximum: 4294967294, 89 | description: "A specific value that guides the 'randomness' of the generation. (Omit or use 0 for random seed)" 90 | }, 91 | outputFormat: { 92 | type: "string", 93 | enum: OUTPUT_FORMATS, 94 | description: "The format of the output image.", 95 | default: "png" 96 | }, 97 | outputImageFileName: { 98 | type: "string", 99 | description: "The desired name of the output image file, no file extension." 100 | } 101 | }, 102 | required: ["prompt", "outputImageFileName"] 103 | } 104 | } as const; 105 | 106 | // Implementation 107 | export const generateImageUltra = async ( 108 | args: GenerateImageUltraArgs, 109 | context: ResourceContext 110 | ) => { 111 | const { 112 | prompt, 113 | aspectRatio, 114 | negativePrompt, 115 | stylePreset, 116 | seed, 117 | outputFormat, 118 | outputImageFileName 119 | } = GenerateImageUltraArgsSchema.parse(args); 120 | 121 | // Capture request parameters for metadata 122 | const requestParams = { 123 | prompt, 124 | aspectRatio, 125 | negativePrompt, 126 | stylePreset, 127 | seed, 128 | outputFormat, 129 | model: "ultra", 130 | outputImageFileName 131 | }; 132 | 133 | try { 134 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY); 135 | 136 | // Make the API call to the Ultra endpoint 137 | const response = await client.generateImageUltra(prompt, { 138 | aspectRatio, 139 | negativePrompt, 140 | stylePreset, 141 | seed, 142 | outputFormat 143 | }); 144 | 145 | const imageAsBase64 = response.base64Image; 146 | const filename = `${outputImageFileName}.${outputFormat}`; 147 | 148 | const resourceClient = getResourceClient(); 149 | const resource = await resourceClient.createResource( 150 | filename, 151 | imageAsBase64, 152 | context 153 | ); 154 | 155 | if (resource.uri.includes("file://")) { 156 | const file_location = resource.uri.replace("file://", ""); 157 | 158 | // Save metadata to a text file 159 | saveMetadata(file_location, requestParams, { 160 | responseType: "success", 161 | timeGenerated: new Date().toISOString() 162 | }); 163 | 164 | open(file_location); 165 | } 166 | 167 | return { 168 | content: [ 169 | { 170 | type: "text", 171 | text: `Processed \`${prompt}\` with Stable Image Ultra to create the following image:`, 172 | }, 173 | { 174 | type: "resource", 175 | resource: resource, 176 | }, 177 | ], 178 | }; 179 | } catch (error) { 180 | // Handle errors and save error metadata if enabled 181 | if (process.env.SAVE_METADATA_FAILED === 'true') { 182 | // Create a temp path for the failed request metadata 183 | const errorFilePath = `${process.env.IMAGE_STORAGE_DIRECTORY}/${outputImageFileName}-failed-${Date.now()}.txt`; 184 | saveMetadata(errorFilePath, requestParams, undefined, error as Error | string); 185 | } 186 | throw error; 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | // Export from generateImageCore.js 2 | export { generateImageCore, generateImageCoreArgs, GenerateImageCoreArgs, generateImageCoreToolDefinition } from "./generateImageCore.js"; 3 | 4 | // Export from generateImageUltra.js 5 | export { generateImageUltra, GenerateImageUltraArgs, generateImageUltraToolDefinition } from "./generateImageUltra.js"; 6 | export * from "./generateImageSD35.js"; 7 | export * from "./removeBackground.js"; 8 | export * from "./outpaint.js"; 9 | export * from "./searchAndReplace.js"; 10 | export * from "./upscaleFast.js"; 11 | export * from "./upscaleCreative.js"; 12 | export * from "./controlSketch.js"; 13 | export * from "./listResources.js"; 14 | export * from "./searchAndRecolor.js"; 15 | export * from "./replaceBackgroundAndRelight.js"; 16 | export * from "./controlStyle.js"; 17 | export * from "./controlStructure.js"; 18 | -------------------------------------------------------------------------------- /src/tools/listResources.ts: -------------------------------------------------------------------------------- 1 | import { ResourceContext } from "../resources/resourceClient.js"; 2 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 3 | 4 | export const listResourcesToolDefinition = { 5 | name: "stability-ai-0-list-resources", 6 | description: 7 | "Use this to check for files before deciding you don't have access to a file or image or resource. It pulls in a list of all of user's available Resources (i.e. image files and their URI's) so we can reference pre-existing images to manipulate or upload to Stability AI.", 8 | inputSchema: { 9 | type: "object", 10 | properties: {}, 11 | required: [], 12 | }, 13 | } as const; 14 | 15 | // List all available Resources via tool. Useful for clients that have limited capability for referencing resources within tool calls. 16 | export const listResources = async (context: ResourceContext) => { 17 | const resourceClient = getResourceClient(); 18 | const resources = await resourceClient.listResources(context); 19 | 20 | return { 21 | content: resources.map((r) => ({ 22 | type: "resource", 23 | resource: { 24 | uri: r.uri, 25 | name: r.name, 26 | mimeType: r.mimeType, 27 | text: `Image: ${r.name} at URI: ${r.uri}`, 28 | }, 29 | })), 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/tools/outpaint.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 3 | import open from "open"; 4 | import { ResourceContext } from "../resources/resourceClient.js"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const OutpaintArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | left: z.number().min(0).max(2000).optional(), 10 | right: z.number().min(0).max(2000).optional(), 11 | up: z.number().min(0).max(2000).optional(), 12 | down: z.number().min(0).max(2000).optional(), 13 | creativity: z.number().min(0).max(1).optional(), 14 | prompt: z.string().max(10000).optional(), 15 | outputImageFileName: z.string(), 16 | }); 17 | 18 | export type OutpaintArgs = z.infer; 19 | 20 | export const outpaintToolDefinition = { 21 | name: "stability-ai-outpaint", 22 | description: `Extends an image in any direction while maintaining visual consistency.`, 23 | inputSchema: { 24 | type: "object", 25 | properties: { 26 | imageFileUri: { 27 | type: "string", 28 | description: `The URI to the image file. It should start with file://`, 29 | }, 30 | left: { 31 | type: "number", 32 | description: "The number of pixels to extend the image to the left", 33 | }, 34 | right: { 35 | type: "number", 36 | description: "The number of pixels to extend the image to the right", 37 | }, 38 | up: { 39 | type: "number", 40 | description: "The number of pixels to extend the image upwards", 41 | }, 42 | down: { 43 | type: "number", 44 | description: "The number of pixels to extend the image downwards", 45 | }, 46 | creativity: { 47 | type: "number", 48 | description: "The creativity of the outpaint operation", 49 | }, 50 | prompt: { 51 | type: "string", 52 | description: "The prompt to use for the outpaint operation", 53 | }, 54 | outputImageFileName: { 55 | type: "string", 56 | description: 57 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 58 | }, 59 | }, 60 | required: ["imageFileUri", "outputImageFileName"], 61 | }, 62 | }; 63 | 64 | export async function outpaint(args: OutpaintArgs, context: ResourceContext) { 65 | const validatedArgs = OutpaintArgsSchema.parse(args); 66 | 67 | const resourceClient = getResourceClient(); 68 | const imageFilePath = await resourceClient.resourceToFile( 69 | validatedArgs.imageFileUri, 70 | context 71 | ); 72 | 73 | // Ensure at least one direction is specified 74 | if ( 75 | !validatedArgs.left && 76 | !validatedArgs.right && 77 | !validatedArgs.up && 78 | !validatedArgs.down 79 | ) { 80 | throw new Error( 81 | "At least one direction (left, right, up, or down) must be specified" 82 | ); 83 | } 84 | 85 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 86 | 87 | const response = await client.outpaint(imageFilePath, validatedArgs); 88 | 89 | const imageAsBase64 = response.base64Image; 90 | const filename = `${validatedArgs.outputImageFileName}.png`; 91 | 92 | const resource = await resourceClient.createResource(filename, imageAsBase64); 93 | 94 | if (resource.uri.includes("file://")) { 95 | const file_location = resource.uri.replace("file://", ""); 96 | open(file_location); 97 | } 98 | 99 | return { 100 | content: [ 101 | { 102 | type: "text", 103 | text: `Processed image "${validatedArgs.imageFileUri}" to outpaint`, 104 | }, 105 | { 106 | type: "resource", 107 | resource: resource, 108 | }, 109 | ], 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /src/tools/removeBackground.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import open from "open"; 4 | import { z } from "zod"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const RemoveBackgroundArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | outputImageFileName: z.string(), 10 | }); 11 | 12 | export type RemoveBackgroundArgs = z.infer; 13 | 14 | export const removeBackgroundToolDefinition = { 15 | name: "stability-ai-remove-background", 16 | description: `Remove the background from an image.`, 17 | inputSchema: { 18 | type: "object", 19 | properties: { 20 | imageFileUri: { 21 | type: "string", 22 | description: `The URI to the image file. It should start with file://`, 23 | }, 24 | outputImageFileName: { 25 | type: "string", 26 | description: 27 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 28 | }, 29 | }, 30 | required: ["imageFileUri"], 31 | }, 32 | }; 33 | 34 | export const removeBackground = async ( 35 | args: RemoveBackgroundArgs, 36 | context: ResourceContext 37 | ) => { 38 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY); 39 | const resourceClient = getResourceClient(); 40 | 41 | const imageFilePath = await resourceClient.resourceToFile(args.imageFileUri); 42 | const response = await client.removeBackground(imageFilePath); 43 | 44 | const imageAsBase64 = response.base64Image; 45 | const filename = `${args.outputImageFileName}.png`; 46 | 47 | const resource = await resourceClient.createResource( 48 | filename, 49 | imageAsBase64, 50 | context 51 | ); 52 | 53 | if (resource.uri.includes("file://")) { 54 | const file_location = resource.uri.replace("file://", ""); 55 | open(file_location); 56 | } 57 | 58 | return { 59 | content: [ 60 | { 61 | type: "text", 62 | text: `Processed image "${args.imageFileUri}" to remove background`, 63 | }, 64 | { 65 | type: "resource", 66 | resource: resource, 67 | }, 68 | ], 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/tools/replaceBackgroundAndRelight.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import open from "open"; 4 | import { z } from "zod"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const ReplaceBackgroundAndRelightArgsSchema = z 8 | .object({ 9 | imageFileUri: z.string(), 10 | backgroundPrompt: z.string().optional(), 11 | backgroundReferenceUri: z.string().optional(), 12 | foregroundPrompt: z.string().optional(), 13 | negativePrompt: z.string().optional(), 14 | preserveOriginalSubject: z.number().min(0).max(1).optional(), 15 | originalBackgroundDepth: z.number().min(0).max(1).optional(), 16 | keepOriginalBackground: z.boolean().optional(), 17 | lightSourceDirection: z 18 | .enum(["above", "below", "left", "right"]) 19 | .optional(), 20 | lightReferenceUri: z.string().optional(), 21 | lightSourceStrength: z.number().min(0).max(1).optional(), 22 | outputImageFileName: z.string(), 23 | }) 24 | .refine((data) => data.backgroundPrompt || data.backgroundReferenceUri, { 25 | message: 26 | "Either backgroundPrompt or backgroundReferenceUri must be provided", 27 | }); 28 | 29 | export type ReplaceBackgroundAndRelightArgs = z.infer< 30 | typeof ReplaceBackgroundAndRelightArgsSchema 31 | >; 32 | 33 | export const replaceBackgroundAndRelightToolDefinition = { 34 | name: "stability-ai-replace-background-and-relight", 35 | description: "Replace background and adjust lighting of an image", 36 | inputSchema: { 37 | type: "object", 38 | properties: { 39 | imageFileUri: { 40 | type: "string", 41 | description: 42 | "The URI to the subject image file. It should start with file://", 43 | }, 44 | backgroundPrompt: { 45 | type: "string", 46 | description: "Description of the desired background", 47 | }, 48 | backgroundReferenceUri: { 49 | type: "string", 50 | description: "Optional URI to a reference image for background style", 51 | }, 52 | foregroundPrompt: { 53 | type: "string", 54 | description: 55 | "Optional description of the subject to prevent background bleeding", 56 | }, 57 | negativePrompt: { 58 | type: "string", 59 | description: "Optional description of what you don't want to see", 60 | }, 61 | preserveOriginalSubject: { 62 | type: "number", 63 | description: "How much to preserve the original subject (0-1)", 64 | }, 65 | originalBackgroundDepth: { 66 | type: "number", 67 | description: "Control background depth matching (0-1)", 68 | }, 69 | keepOriginalBackground: { 70 | type: "boolean", 71 | description: "Whether to keep the original background", 72 | }, 73 | lightSourceDirection: { 74 | type: "string", 75 | enum: ["above", "below", "left", "right"], 76 | description: "Direction of the light source", 77 | }, 78 | lightReferenceUri: { 79 | type: "string", 80 | description: "Optional URI to a reference image for lighting", 81 | }, 82 | lightSourceStrength: { 83 | type: "number", 84 | description: "Strength of the light source (0-1)", 85 | }, 86 | outputImageFileName: { 87 | type: "string", 88 | description: 89 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 90 | }, 91 | }, 92 | required: ["imageFileUri", "outputImageFileName"], 93 | }, 94 | }; 95 | 96 | export const replaceBackgroundAndRelight = async ( 97 | args: ReplaceBackgroundAndRelightArgs, 98 | context: ResourceContext 99 | ) => { 100 | const validatedArgs = ReplaceBackgroundAndRelightArgsSchema.parse(args); 101 | 102 | const resourceClient = getResourceClient(); 103 | const imageFilePath = await resourceClient.resourceToFile( 104 | validatedArgs.imageFileUri, 105 | context 106 | ); 107 | 108 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 109 | 110 | // Convert file URIs to file paths for reference images if provided 111 | let backgroundReference: string | undefined; 112 | if (validatedArgs.backgroundReferenceUri) { 113 | backgroundReference = await resourceClient.resourceToFile( 114 | validatedArgs.backgroundReferenceUri 115 | ); 116 | } 117 | 118 | let lightReference: string | undefined; 119 | if (validatedArgs.lightReferenceUri) { 120 | lightReference = await resourceClient.resourceToFile( 121 | validatedArgs.lightReferenceUri 122 | ); 123 | } 124 | 125 | const response = await client.replaceBackgroundAndRelight(imageFilePath, { 126 | backgroundPrompt: validatedArgs.backgroundPrompt, 127 | backgroundReference, 128 | foregroundPrompt: validatedArgs.foregroundPrompt, 129 | negativePrompt: validatedArgs.negativePrompt, 130 | preserveOriginalSubject: validatedArgs.preserveOriginalSubject, 131 | originalBackgroundDepth: validatedArgs.originalBackgroundDepth, 132 | keepOriginalBackground: validatedArgs.keepOriginalBackground, 133 | lightSourceDirection: validatedArgs.lightSourceDirection, 134 | lightReference, 135 | lightSourceStrength: validatedArgs.lightSourceStrength, 136 | }); 137 | 138 | const imageAsBase64 = response.base64Image; 139 | const filename = `${validatedArgs.outputImageFileName}.png`; 140 | 141 | const resource = await resourceClient.createResource(filename, imageAsBase64); 142 | 143 | if (resource.uri.includes("file://")) { 144 | const file_location = resource.uri.replace("file://", ""); 145 | open(file_location); 146 | } 147 | 148 | return { 149 | content: [ 150 | { 151 | type: "text", 152 | text: `Processed image "${validatedArgs.imageFileUri}" with background and lighting adjustments`, 153 | }, 154 | { 155 | type: "resource", 156 | resource: resource, 157 | }, 158 | ], 159 | }; 160 | }; 161 | -------------------------------------------------------------------------------- /src/tools/searchAndRecolor.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import open from "open"; 4 | import { z } from "zod"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const SearchAndRecolorArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | prompt: z.string(), 10 | selectPrompt: z.string(), 11 | outputImageFileName: z.string(), 12 | }); 13 | 14 | export type SearchAndRecolorArgs = z.infer; 15 | 16 | export const searchAndRecolorToolDefinition = { 17 | name: "stability-ai-search-and-recolor", 18 | description: "Search and recolor object(s) in an image", 19 | inputSchema: { 20 | type: "object", 21 | properties: { 22 | imageFileUri: { 23 | type: "string", 24 | description: "The URI to the image file. It should start with file://", 25 | }, 26 | prompt: { 27 | type: "string", 28 | description: "What colors you wish to see in the output image", 29 | }, 30 | selectPrompt: { 31 | type: "string", 32 | description: 33 | "Short description of what to search for and recolor in the image", 34 | }, 35 | outputImageFileName: { 36 | type: "string", 37 | description: 38 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 39 | }, 40 | }, 41 | required: ["imageFileUri", "prompt", "selectPrompt", "outputImageFileName"], 42 | }, 43 | }; 44 | 45 | export const searchAndRecolor = async ( 46 | args: SearchAndRecolorArgs, 47 | context: ResourceContext 48 | ) => { 49 | const validatedArgs = SearchAndRecolorArgsSchema.parse(args); 50 | 51 | const resourceClient = getResourceClient(); 52 | const imageFilePath = await resourceClient.resourceToFile( 53 | validatedArgs.imageFileUri, 54 | context 55 | ); 56 | 57 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 58 | 59 | const response = await client.searchAndRecolor(imageFilePath, { 60 | prompt: validatedArgs.prompt, 61 | selectPrompt: validatedArgs.selectPrompt, 62 | }); 63 | 64 | const imageAsBase64 = response.base64Image; 65 | const filename = `${validatedArgs.outputImageFileName}.png`; 66 | 67 | const resource = await resourceClient.createResource(filename, imageAsBase64); 68 | 69 | if (resource.uri.includes("file://")) { 70 | const file_location = resource.uri.replace("file://", ""); 71 | open(file_location); 72 | } 73 | 74 | return { 75 | content: [ 76 | { 77 | type: "text", 78 | text: `Processed image "${validatedArgs.imageFileUri}" to recolor "${validatedArgs.selectPrompt}" with "${validatedArgs.prompt}"`, 79 | }, 80 | { 81 | type: "resource", 82 | resource: resource, 83 | }, 84 | ], 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /src/tools/searchAndReplace.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import { ResourceContext } from "../resources/resourceClient.js"; 3 | import open from "open"; 4 | import { z } from "zod"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const SearchAndReplaceArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | searchPrompt: z.string().max(10000), 10 | prompt: z.string().max(10000), 11 | outputImageFileName: z.string(), 12 | }); 13 | 14 | export type SearchAndReplaceArgs = z.infer; 15 | 16 | export const searchAndReplaceToolDefinition = { 17 | name: "stability-ai-search-and-replace", 18 | description: `Replace objects or elements in an image by describing what to replace and what to replace it with.`, 19 | inputSchema: { 20 | type: "object", 21 | properties: { 22 | imageFileUri: { 23 | type: "string", 24 | description: `The URI to the image file. It should start with file://`, 25 | }, 26 | searchPrompt: { 27 | type: "string", 28 | description: "Short description of what to replace in the image", 29 | }, 30 | prompt: { 31 | type: "string", 32 | description: "What you wish to see in place of the searched content", 33 | }, 34 | outputImageFileName: { 35 | type: "string", 36 | description: 37 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 38 | }, 39 | }, 40 | required: ["imageFileUri", "searchPrompt", "prompt", "outputImageFileName"], 41 | }, 42 | }; 43 | 44 | export async function searchAndReplace( 45 | args: SearchAndReplaceArgs, 46 | context: ResourceContext 47 | ) { 48 | const validatedArgs = SearchAndReplaceArgsSchema.parse(args); 49 | 50 | const resourceClient = getResourceClient(); 51 | const imageFilePath = await resourceClient.resourceToFile( 52 | validatedArgs.imageFileUri, 53 | context 54 | ); 55 | 56 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 57 | 58 | const response = await client.searchAndReplace(imageFilePath, validatedArgs); 59 | 60 | const imageAsBase64 = response.base64Image; 61 | const filename = `${validatedArgs.outputImageFileName}.png`; 62 | 63 | const resource = await resourceClient.createResource(filename, imageAsBase64); 64 | 65 | if (resource.uri.includes("file://")) { 66 | const file_location = resource.uri.replace("file://", ""); 67 | open(file_location); 68 | } 69 | 70 | return { 71 | content: [ 72 | { 73 | type: "text", 74 | text: `Processed image "${validatedArgs.imageFileUri}" to replace "${validatedArgs.searchPrompt}" with "${validatedArgs.prompt}"`, 75 | }, 76 | { 77 | type: "resource", 78 | resource: resource, 79 | }, 80 | ], 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /src/tools/upscaleCreative.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import open from "open"; 3 | import { z } from "zod"; 4 | import { ResourceContext } from "../resources/resourceClient.js"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const UpscaleCreativeArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | prompt: z.string(), 10 | negativePrompt: z.string().optional(), 11 | creativity: z.number().min(0).max(0.35).optional(), 12 | outputImageFileName: z.string(), 13 | }); 14 | 15 | export type UpscaleCreativeArgs = z.infer; 16 | 17 | export const upscaleCreativeToolDefinition = { 18 | name: "stability-ai-upscale-creative", 19 | description: `Enhance image resolution up to 4K using AI with creative interpretation. This tool works best on highly degraded images and performs heavy reimagining. In general, don't use this (expensive) tool unless specifically asked to do so, usually after trying stability-ai-upscale-fast first.`, 20 | inputSchema: { 21 | type: "object", 22 | properties: { 23 | imageFileUri: { 24 | type: "string", 25 | description: `The URI to the image file. It should start with file://`, 26 | }, 27 | prompt: { 28 | type: "string", 29 | description: 30 | "What you wish to see in the output image. A strong, descriptive prompt that clearly defines elements, colors, and subjects.", 31 | }, 32 | negativePrompt: { 33 | type: "string", 34 | description: 35 | "Optional text describing what you do not wish to see in the output image.", 36 | }, 37 | creativity: { 38 | type: "number", 39 | description: 40 | "Optional value (0-0.35) indicating how creative the model should be. Higher values add more details during upscaling.", 41 | }, 42 | outputImageFileName: { 43 | type: "string", 44 | description: 45 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 46 | }, 47 | }, 48 | required: ["imageFileUri", "prompt", "outputImageFileName"], 49 | }, 50 | }; 51 | 52 | export async function upscaleCreative( 53 | args: UpscaleCreativeArgs, 54 | context: ResourceContext 55 | ) { 56 | const validatedArgs = UpscaleCreativeArgsSchema.parse(args); 57 | 58 | const resourceClient = getResourceClient(); 59 | const imageFilePath = await resourceClient.resourceToFile( 60 | validatedArgs.imageFileUri, 61 | context 62 | ); 63 | 64 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 65 | 66 | try { 67 | const response = await client.upscaleCreative(imageFilePath, { 68 | prompt: validatedArgs.prompt, 69 | negativePrompt: validatedArgs.negativePrompt, 70 | creativity: validatedArgs.creativity, 71 | }); 72 | 73 | const imageAsBase64 = response.base64Image; 74 | const filename = `${validatedArgs.outputImageFileName}.png`; 75 | 76 | const resource = await resourceClient.createResource( 77 | filename, 78 | imageAsBase64 79 | ); 80 | 81 | if (resource.uri.includes("file://")) { 82 | const file_location = resource.uri.replace("file://", ""); 83 | open(file_location); 84 | } 85 | 86 | return { 87 | content: [ 88 | { 89 | type: "text", 90 | text: `Processed image "${validatedArgs.imageFileUri}" with creative upscaling`, 91 | }, 92 | { 93 | type: "resource", 94 | resource: resource, 95 | }, 96 | ], 97 | }; 98 | } catch (error) { 99 | const errorMessage = 100 | error instanceof Error ? error.message : "Unknown error"; 101 | return { 102 | content: [ 103 | { 104 | type: "text", 105 | text: `Failed to upscale image: ${errorMessage}`, 106 | }, 107 | ], 108 | }; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/tools/upscaleFast.ts: -------------------------------------------------------------------------------- 1 | import { StabilityAiApiClient } from "../stabilityAi/stabilityAiApiClient.js"; 2 | import open from "open"; 3 | import { z } from "zod"; 4 | import { ResourceContext } from "../resources/resourceClient.js"; 5 | import { getResourceClient } from "../resources/resourceClientFactory.js"; 6 | 7 | const UpscaleFastArgsSchema = z.object({ 8 | imageFileUri: z.string(), 9 | outputImageFileName: z.string(), 10 | }); 11 | 12 | export type UpscaleFastArgs = z.infer; 13 | 14 | export const upscaleFastToolDefinition = { 15 | name: "stability-ai-upscale-fast", 16 | description: `Cheap and fast tool to enhance image resolution by 4x.`, 17 | inputSchema: { 18 | type: "object", 19 | properties: { 20 | imageFileUri: { 21 | type: "string", 22 | description: `The URI to the image file. It should start with file://`, 23 | }, 24 | outputImageFileName: { 25 | type: "string", 26 | description: 27 | "The desired name of the output image file, no file extension. Make it descriptive but short. Lowercase, dash-separated, no special characters.", 28 | }, 29 | }, 30 | required: ["imageFileUri", "outputImageFileName"], 31 | }, 32 | }; 33 | 34 | export async function upscaleFast( 35 | args: UpscaleFastArgs, 36 | context: ResourceContext 37 | ) { 38 | const validatedArgs = UpscaleFastArgsSchema.parse(args); 39 | 40 | const resourceClient = getResourceClient(); 41 | const imageFilePath = await resourceClient.resourceToFile( 42 | validatedArgs.imageFileUri, 43 | context 44 | ); 45 | 46 | const client = new StabilityAiApiClient(process.env.STABILITY_AI_API_KEY!); 47 | 48 | try { 49 | const response = await client.upscaleFast(imageFilePath); 50 | 51 | const imageAsBase64 = response.base64Image; 52 | const filename = `${validatedArgs.outputImageFileName}.png`; 53 | 54 | const resource = await resourceClient.createResource( 55 | filename, 56 | imageAsBase64 57 | ); 58 | 59 | if (resource.uri.includes("file://")) { 60 | const file_location = resource.uri.replace("file://", ""); 61 | open(file_location); 62 | } 63 | 64 | return { 65 | content: [ 66 | { 67 | type: "text", 68 | text: `Processed image "${validatedArgs.imageFileUri}" to upscale by 4x`, 69 | }, 70 | { 71 | type: "resource", 72 | resource: resource, 73 | }, 74 | { 75 | type: "text", 76 | text: `Let the user know that they can now call upscale-creative to further increase the quality of the image if this result isn't good enough for them.`, 77 | }, 78 | ], 79 | }; 80 | } catch (error) { 81 | const errorMessage = 82 | error instanceof Error ? error.message : "Unknown error"; 83 | return { 84 | content: [ 85 | { 86 | type: "text", 87 | text: `Failed to upscale image: ${errorMessage}`, 88 | }, 89 | ], 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*", "*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | --------------------------------------------------------------------------------