├── .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 |

6 |
7 |
8 |
9 |
10 |
11 |

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 | 
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 |
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 |
224 |
225 |
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 |
--------------------------------------------------------------------------------