pixellab-forge-mcp 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/api-client.d.ts +9 -0
- package/dist/api-client.js +89 -0
- package/dist/api-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/job-log.d.ts +16 -0
- package/dist/job-log.js +88 -0
- package/dist/job-log.js.map +1 -0
- package/dist/prompts.d.ts +21 -0
- package/dist/prompts.js +290 -0
- package/dist/prompts.js.map +1 -0
- package/dist/save-images.d.ts +14 -0
- package/dist/save-images.js +242 -0
- package/dist/save-images.js.map +1 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +1072 -0
- package/dist/tools.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rabbitcannon
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# PixelLab Forge MCP
|
|
2
|
+
|
|
3
|
+
An MCP server that connects AI assistants to the [PixelLab](https://pixellab.ai) pixel art generation API. Generate sprites, tilesets, characters, animations, and more directly from Claude, Cursor, or any MCP-compatible client.
|
|
4
|
+
|
|
5
|
+
Generated images are automatically saved to `./pixellab-forge-output/` in your project directory, ready to be moved into your game assets.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- **Node.js** 18 or later
|
|
10
|
+
- A **PixelLab API key** — get one at [pixellab.ai/account](https://pixellab.ai/account)
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
No installation needed. `npx` downloads and runs the package automatically on first use.
|
|
15
|
+
|
|
16
|
+
### Claude Code (CLI)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
claude mcp add pixellab-forge-mcp -e PIXELLAB_API_KEY=your-api-key-here -- npx pixellab-forge-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This adds it to the current project. To make it available across all your projects:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude mcp add pixellab-forge-mcp -s user -e PIXELLAB_API_KEY=your-api-key-here -- npx pixellab-forge-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it. Claude Code will start the server automatically when you begin a conversation.
|
|
29
|
+
|
|
30
|
+
### Claude Desktop
|
|
31
|
+
|
|
32
|
+
Add to your config file:
|
|
33
|
+
|
|
34
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
35
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"pixellab-forge-mcp": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["pixellab-forge-mcp"],
|
|
43
|
+
"env": {
|
|
44
|
+
"PIXELLAB_API_KEY": "your-api-key-here"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Cursor
|
|
52
|
+
|
|
53
|
+
Add to `.cursor/mcp.json`:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"pixellab-forge-mcp": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["pixellab-forge-mcp"],
|
|
61
|
+
"env": {
|
|
62
|
+
"PIXELLAB_API_KEY": "your-api-key-here"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Other MCP Clients
|
|
70
|
+
|
|
71
|
+
Any MCP client that supports stdio transport can use PixelLab Forge. Set the command to `npx pixellab-forge-mcp` and pass `PIXELLAB_API_KEY` as an environment variable.
|
|
72
|
+
|
|
73
|
+
## Generated Images
|
|
74
|
+
|
|
75
|
+
When a tool returns image data, PixelLab Forge automatically saves the images as PNGs to `./pixellab-forge-output/` in whatever directory the MCP server is running from (usually your project root).
|
|
76
|
+
|
|
77
|
+
Add this to your `.gitignore`:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
pixellab-forge-output/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Available Tools (47)
|
|
84
|
+
|
|
85
|
+
Generation tools automatically poll for results — no manual job status checking needed. If a job takes longer than 10 minutes or the connection drops, use `list_pending_jobs` to find the job ID and `get_job_status` to retrieve the result.
|
|
86
|
+
|
|
87
|
+
### Image Generation
|
|
88
|
+
|
|
89
|
+
| Tool | Description | Key Options |
|
|
90
|
+
|------|-------------|-------------|
|
|
91
|
+
| `generate_image` | Generate pixel art from text | `reference_images`, `style_image`, `style_options`, `no_background`, `seed` |
|
|
92
|
+
| `generate_with_style` | Match style from 1-4 references | `style_images`, `style_description`, `no_background`, `seed` |
|
|
93
|
+
| `generate_ui` | Game UI elements (buttons, panels, icons) | `concept_image`, `color_palette`, `no_background`, `seed` |
|
|
94
|
+
| `create_image_pixflux` | Pixflux engine (32-400px) | `text_guidance_scale`, `init_image`, `color_image`, `no_background`, `isometric`, `outline/shading/detail`, `seed` |
|
|
95
|
+
| `create_image_bitforge` | Bitforge engine (max 200px) | `text_guidance_scale`, `style_image`, `inpainting_image`, `mask_image`, `color_image`, `skeleton_keypoints`, `outline/shading/detail`, `seed` |
|
|
96
|
+
|
|
97
|
+
### Characters & Objects
|
|
98
|
+
|
|
99
|
+
| Tool | Description | Key Options |
|
|
100
|
+
|------|-------------|-------------|
|
|
101
|
+
| `create_character_4dir` | Character with N/S/E/W views | `proportions`, `view`, `text_guidance_scale`, `outline/shading/detail`, `color_image`, `force_colors`, `template_id`, `isometric`, `seed` |
|
|
102
|
+
| `create_character_8dir` | Character with 8 directional views | Same as 4dir |
|
|
103
|
+
| `animate_character` | Animate existing character | `template_animation_id` (walking, fireball, breathing-idle, etc. — [47 templates](docs/prompting-guide.md#animation-templates)), `action_description`, `directions`, `outline/shading/detail`, `seed` |
|
|
104
|
+
| `create_object_4dir` | Object with 4 directional views | `view`, `text_guidance_scale`, `outline/shading/detail`, `color_image`, `force_colors`, `seed` |
|
|
105
|
+
| `list_characters` / `list_objects` | List with pagination | `limit`, `offset` |
|
|
106
|
+
| `get_character` / `get_object` | Get details by ID | |
|
|
107
|
+
| `delete_character` / `delete_object` | Delete by ID | |
|
|
108
|
+
| `download_character_zip` | Export character as ZIP | |
|
|
109
|
+
| `update_character_tags` / `update_object_tags` | Manage tags | |
|
|
110
|
+
|
|
111
|
+
### Animation
|
|
112
|
+
|
|
113
|
+
| Tool | Description | Key Options |
|
|
114
|
+
|------|-------------|-------------|
|
|
115
|
+
| `animate_with_text` | Animate from text + reference | `text_guidance_scale`, `image_guidance_scale`, `n_frames`, `init_images`, `color_image`, `seed` |
|
|
116
|
+
| `animate_with_text_v2` | Animate existing image (32-256px) | `reference_image`, `action`, `view`, `direction`, `no_background`, `seed` |
|
|
117
|
+
| `animate_with_text_v3` | Animate from first/last keyframes | `first_frame`, `last_frame`, `frame_count`, `no_background`, `seed` |
|
|
118
|
+
| `animate_with_skeleton` | Pose control via keypoints | `skeleton_keypoints`, `reference_guidance_scale`, `pose_guidance_scale`, `isometric`, `color_image`, `seed` |
|
|
119
|
+
| `edit_animation` | Edit animation frames (2-16) | `frames`, `description`, `no_background`, `seed` |
|
|
120
|
+
| `interpolate_frames` | Generate in-between frames | `start_image`, `end_image`, `action`, `no_background`, `seed` |
|
|
121
|
+
| `transfer_outfit` | Apply outfit to frames | `reference_image`, `frames`, `no_background`, `seed` |
|
|
122
|
+
| `estimate_skeleton` | Extract keypoints from image | `image`, `image_size` |
|
|
123
|
+
|
|
124
|
+
### Rotation
|
|
125
|
+
|
|
126
|
+
| Tool | Description | Key Options |
|
|
127
|
+
|------|-------------|-------------|
|
|
128
|
+
| `generate_8_rotations` | 8 directional views (32-168px) | `method` (rotate/style/concept), `view`, `style_description`, `no_background`, `seed` |
|
|
129
|
+
| `rotate` | Rotate between views/directions | `from_view/to_view`, `from_direction/to_direction`, `view_change`, `direction_change`, `image_guidance_scale`, `isometric`, `seed` |
|
|
130
|
+
|
|
131
|
+
### Editing & Inpainting
|
|
132
|
+
|
|
133
|
+
| Tool | Description | Key Options |
|
|
134
|
+
|------|-------------|-------------|
|
|
135
|
+
| `edit_images` | Batch edit 1-16 images | `method` (text/reference), `description`, `reference_image`, `no_background`, `seed` |
|
|
136
|
+
| `edit_image` | Edit single image | `image`, `description`, `width`, `height`, `text_guidance_scale`, `color_image`, `no_background`, `seed` |
|
|
137
|
+
| `inpaint_v3` | Mask-based editing | `mask_image`, `bounding_box`, `crop_to_mask`, `no_background`, `seed` |
|
|
138
|
+
| `inpaint` | Inpainting (legacy, max 200px) | `mask_image`, `text_guidance_scale`, `outline/shading/detail`, `isometric`, `color_image`, `seed` |
|
|
139
|
+
|
|
140
|
+
### Image Operations
|
|
141
|
+
|
|
142
|
+
| Tool | Description | Key Options |
|
|
143
|
+
|------|-------------|-------------|
|
|
144
|
+
| `image_to_pixelart` | Convert photo to pixel art | `image`, `output_size` |
|
|
145
|
+
| `resize_image` | AI-powered pixel art resize | `reference_image`, `target_size`, `color_image` |
|
|
146
|
+
| `remove_background` | Remove background (max 400px) | `background_removal_task`, `text_hint` |
|
|
147
|
+
|
|
148
|
+
### Tilesets
|
|
149
|
+
|
|
150
|
+
| Tool | Description | Key Options |
|
|
151
|
+
|------|-------------|-------------|
|
|
152
|
+
| `create_tileset` | Top-down tileset (16 or 32px) | `lower/upper/transition_description`, `tile_size`, `view`, `text_guidance_scale`, `tile_strength`, `tileset_adherence`, `lower/upper/transition_reference_image`, `seed` |
|
|
153
|
+
| `create_tileset_sidescroller` | Platformer tileset | `lower_description`, `transition_description`, `text_guidance_scale`, `tile_strength`, `tileset_adherence`, `base_tile_id`, `seed` |
|
|
154
|
+
| `create_isometric_tile` | Isometric tile (16-64px) | `isometric_tile_shape` (block/thick/thin), `text_guidance_scale`, `outline/shading/detail`, `color_image`, `seed` |
|
|
155
|
+
| `create_tiles_pro` | Pro tiles (hex, iso, octagon, square) | `tile_type`, `tile_size`, `tile_view`, `tile_depth_ratio`, `style_images`, `style_options`, `n_tiles`, `seed` |
|
|
156
|
+
| `get_tileset` / `get_isometric_tile` / `get_tiles_pro` | Retrieve by ID | |
|
|
157
|
+
|
|
158
|
+
### Map Objects
|
|
159
|
+
|
|
160
|
+
| Tool | Description | Key Options |
|
|
161
|
+
|------|-------------|-------------|
|
|
162
|
+
| `create_map_object` | Game-ready object | `view`, `outline/shading/detail`, `text_guidance_scale`, `background_image`, `inpainting`, `color_image`, `seed` |
|
|
163
|
+
|
|
164
|
+
### Account & Jobs
|
|
165
|
+
|
|
166
|
+
| Tool | Description |
|
|
167
|
+
|------|-------------|
|
|
168
|
+
| `get_balance` | Check your credit balance |
|
|
169
|
+
| `get_job_status` | Check a background job by ID |
|
|
170
|
+
| `list_pending_jobs` | List jobs that haven't completed (for recovery after disconnection) |
|
|
171
|
+
| `list_job_history` | Recent job history (auto-pruned after 24h) |
|
|
172
|
+
|
|
173
|
+
### Common Options
|
|
174
|
+
|
|
175
|
+
Most tools share these parameters, but the field names differ between endpoint generations:
|
|
176
|
+
|
|
177
|
+
| Concept | v2 endpoints | Legacy endpoints |
|
|
178
|
+
|---------|-------------|------------------|
|
|
179
|
+
| Prompt adherence | n/a | `text_guidance_scale` (1-20) |
|
|
180
|
+
| Transparent background | `no_background` | `no_background` |
|
|
181
|
+
| Color reference | n/a | `color_image` (reference image) |
|
|
182
|
+
| Style controls | n/a | `outline`, `shading`, `detail` |
|
|
183
|
+
| Negative prompt | n/a | `negative_description` |
|
|
184
|
+
| Isometric mode | n/a | `isometric`, `oblique_projection` |
|
|
185
|
+
| Reproducibility | `seed` | `seed` |
|
|
186
|
+
|
|
187
|
+
Character, object, tileset, and map object endpoints also accept `text_guidance_scale`, `outline`, `shading`, `detail`, and `color_image`.
|
|
188
|
+
|
|
189
|
+
## Usage
|
|
190
|
+
|
|
191
|
+
Just describe what you want in plain language. The assistant picks the right tool and parameters automatically.
|
|
192
|
+
|
|
193
|
+
### Quick Examples
|
|
194
|
+
|
|
195
|
+
**Sprites and icons:**
|
|
196
|
+
```
|
|
197
|
+
"Generate a 64x64 pixel art knight with a blue cape, no background"
|
|
198
|
+
"Make a 16x16 health potion icon"
|
|
199
|
+
"Create a 128x128 dragon boss with detailed shading and thick outlines"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Characters (persistent, multi-directional):**
|
|
203
|
+
```
|
|
204
|
+
"Create a 48x48 character with 4 directions: a dwarf blacksmith in a leather apron, chibi proportions"
|
|
205
|
+
"Animate that character with the walk template"
|
|
206
|
+
"Now add a fireball animation"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Tilesets:**
|
|
210
|
+
```
|
|
211
|
+
"Create a 32x32 top-down tileset: ocean water below, sandy beach on top, foam transition"
|
|
212
|
+
"Make a 16x16 sidescroller tileset with stone platforms"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Editing existing art:**
|
|
216
|
+
```
|
|
217
|
+
"Edit this sprite to make the armor gold instead of silver"
|
|
218
|
+
"Remove the background from this image"
|
|
219
|
+
"Generate 8 rotations of this character"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Which Tool Gets Used?
|
|
223
|
+
|
|
224
|
+
The assistant picks the right tool automatically, but the key decision is:
|
|
225
|
+
|
|
226
|
+
- **`generate_image`** — default for most requests, highest quality, largest sizes
|
|
227
|
+
- **`generate_with_style`** — when you want new art matching existing art ("in the same style as these sprites")
|
|
228
|
+
- **`generate_ui`** — for game UI elements (buttons, panels, health bars, icons)
|
|
229
|
+
- **`create_tileset` / `create_tiles_pro`** — for tileable terrain; standard for square RPG tiles, pro for hex/isometric/octagon
|
|
230
|
+
- **`create_character_4dir` / `8dir`** — for persistent characters you can animate later by ID
|
|
231
|
+
|
|
232
|
+
### Key Concepts
|
|
233
|
+
|
|
234
|
+
- **Sizes are in pixels** as `width x height` — different tools have different limits (e.g. characters max 128x128, `generate_image` goes up to 792x688)
|
|
235
|
+
- **Transparent backgrounds** are the default on most tools — ask for a background explicitly if you want one
|
|
236
|
+
- **Characters are persistent** — once created, you can animate them by ID without re-describing
|
|
237
|
+
- **Seeds** make results reproducible — same seed + same description = same output
|
|
238
|
+
|
|
239
|
+
For detailed size limits, style controls, endpoint comparison, and step-by-step workflows, see the **[Prompting Guide](docs/prompting-guide.md)**.
|
|
240
|
+
|
|
241
|
+
### Prompt Commands
|
|
242
|
+
|
|
243
|
+
PixelLab Forge includes MCP prompt templates that appear as slash commands in supported clients (Claude Desktop, Cursor, etc.):
|
|
244
|
+
|
|
245
|
+
| Command | Description |
|
|
246
|
+
|---------|-------------|
|
|
247
|
+
| `pf:help` | Overview of all tools and how to use them |
|
|
248
|
+
| `pf:sprite` | Generate a pixel art sprite |
|
|
249
|
+
| `pf:character` | Create a character with directional views + animation |
|
|
250
|
+
| `pf:animate` | Animate an existing sprite or character |
|
|
251
|
+
| `pf:tileset` | Create a top-down or sidescroller tileset |
|
|
252
|
+
| `pf:tiles` | Create hex, isometric, or octagon tiles |
|
|
253
|
+
| `pf:ui` | Generate game UI elements |
|
|
254
|
+
| `pf:style` | Generate art matching existing sprites' style |
|
|
255
|
+
| `pf:edit` | Edit or modify an existing sprite |
|
|
256
|
+
|
|
257
|
+
## Reliability
|
|
258
|
+
|
|
259
|
+
- **Auto-polling**: Generation jobs are polled every 2 seconds for up to 10 minutes
|
|
260
|
+
- **Retry on failure**: Network errors during polling are retried 3 times with backoff
|
|
261
|
+
- **Job recovery**: If the connection drops, job IDs are logged to stderr and persisted to disk. Use `list_pending_jobs` to find them and `get_job_status` to retrieve results
|
|
262
|
+
- **Image saving**: Generated images are automatically saved as PNGs to `./pixellab-forge-output/`
|
|
263
|
+
|
|
264
|
+
## Testing
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Set `PIXELLAB_API_KEY` in the inspector's environment variables, connect, and test any tool.
|
|
271
|
+
|
|
272
|
+
## Contributing
|
|
273
|
+
|
|
274
|
+
Contributions are welcome via pull requests.
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class PixelLabClient {
|
|
2
|
+
private apiKey;
|
|
3
|
+
constructor(apiKey: string);
|
|
4
|
+
private headers;
|
|
5
|
+
get(path: string): Promise<unknown>;
|
|
6
|
+
delete(path: string): Promise<unknown>;
|
|
7
|
+
patch(path: string, body: unknown): Promise<unknown>;
|
|
8
|
+
post(path: string, body: unknown): Promise<unknown>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { logJobStart } from "./job-log.js";
|
|
2
|
+
const BASE_URL = "https://api.pixellab.ai/v2";
|
|
3
|
+
function debugLog(msg) {
|
|
4
|
+
process.stderr.write(`[pixellab-forge-mcp] ${msg}\n`);
|
|
5
|
+
}
|
|
6
|
+
export class PixelLabClient {
|
|
7
|
+
apiKey;
|
|
8
|
+
constructor(apiKey) {
|
|
9
|
+
this.apiKey = apiKey;
|
|
10
|
+
}
|
|
11
|
+
headers(contentType = "application/json") {
|
|
12
|
+
return {
|
|
13
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
14
|
+
"Content-Type": contentType,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async get(path) {
|
|
18
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
19
|
+
method: "GET",
|
|
20
|
+
headers: this.headers(),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const text = await res.text();
|
|
24
|
+
throw new Error(`GET ${path} failed (${res.status}): ${text}`);
|
|
25
|
+
}
|
|
26
|
+
return res.json();
|
|
27
|
+
}
|
|
28
|
+
async delete(path) {
|
|
29
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
30
|
+
method: "DELETE",
|
|
31
|
+
headers: this.headers(),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
throw new Error(`DELETE ${path} failed (${res.status}): ${text}`);
|
|
36
|
+
}
|
|
37
|
+
if (res.status === 204)
|
|
38
|
+
return { success: true };
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
async patch(path, body) {
|
|
42
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
43
|
+
method: "PATCH",
|
|
44
|
+
headers: this.headers(),
|
|
45
|
+
body: JSON.stringify(body),
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
throw new Error(`PATCH ${path} failed (${res.status}): ${text}`);
|
|
50
|
+
}
|
|
51
|
+
return res.json();
|
|
52
|
+
}
|
|
53
|
+
async post(path, body) {
|
|
54
|
+
const jsonBody = JSON.stringify(body);
|
|
55
|
+
debugLog(`POST ${path} — payload size: ${jsonBody.length} bytes`);
|
|
56
|
+
// Extract description for job logging
|
|
57
|
+
const desc = body && typeof body === "object"
|
|
58
|
+
? body.description
|
|
59
|
+
?? body.text
|
|
60
|
+
: undefined;
|
|
61
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: this.headers(),
|
|
64
|
+
body: jsonBody,
|
|
65
|
+
});
|
|
66
|
+
debugLog(`POST ${path} — response status: ${res.status}`);
|
|
67
|
+
if (res.status === 200) {
|
|
68
|
+
return res.json();
|
|
69
|
+
}
|
|
70
|
+
if (res.status === 202) {
|
|
71
|
+
const data = (await res.json());
|
|
72
|
+
const jobId = data.job_id ?? data.background_job_id;
|
|
73
|
+
debugLog(`POST ${path} — background job: ${jobId}`);
|
|
74
|
+
if (jobId) {
|
|
75
|
+
logJobStart(jobId, path, desc);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
status: "processing",
|
|
79
|
+
job_id: jobId,
|
|
80
|
+
endpoint: path,
|
|
81
|
+
message: `Job ${jobId} is processing. Use get_job_status with this job_id to check progress and retrieve results.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const text = await res.text();
|
|
85
|
+
debugLog(`POST ${path} — ERROR ${res.status}: ${text}`);
|
|
86
|
+
throw new Error(`POST ${path} failed (${res.status}): ${text}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=api-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { PixelLabClient } from "./api-client.js";
|
|
6
|
+
import { tools } from "./tools.js";
|
|
7
|
+
import { prompts } from "./prompts.js";
|
|
8
|
+
import { extractAndSaveImages } from "./save-images.js";
|
|
9
|
+
import { getJobEndpoint, getJobDescription } from "./job-log.js";
|
|
10
|
+
function slugify(text) {
|
|
11
|
+
return text
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
14
|
+
.replace(/^_|_$/g, "")
|
|
15
|
+
.slice(0, 60);
|
|
16
|
+
}
|
|
17
|
+
const apiKey = process.env["PIXELLAB_API_KEY"];
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
console.error("Error: PIXELLAB_API_KEY environment variable is required.");
|
|
20
|
+
console.error("Get your API key from https://pixellab.ai/account");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const client = new PixelLabClient(apiKey);
|
|
24
|
+
const server = new Server({ name: "pixellab-forge-mcp", version: "1.2.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
25
|
+
const toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
26
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
27
|
+
tools: tools.map((t) => ({
|
|
28
|
+
name: t.name,
|
|
29
|
+
description: t.description,
|
|
30
|
+
inputSchema: t.inputSchema,
|
|
31
|
+
})),
|
|
32
|
+
}));
|
|
33
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
34
|
+
const { name } = request.params;
|
|
35
|
+
const args = request.params.arguments ?? {};
|
|
36
|
+
const tool = toolMap.get(name);
|
|
37
|
+
if (!tool) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
40
|
+
isError: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const result = await tool.handler(client, args);
|
|
45
|
+
process.stderr.write(`[pixellab-forge-mcp] Raw response keys for ${name}: ${result && typeof result === "object" ? JSON.stringify(Object.keys(result)) : typeof result}\n`);
|
|
46
|
+
// read_image returns raw base64 for use in subsequent tool calls — skip extraction/stripping
|
|
47
|
+
if (name === "read_image") {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Derive a meaningful filename from the prompt description when available
|
|
53
|
+
let imageLabel = name;
|
|
54
|
+
const desc = args.description ?? args.text;
|
|
55
|
+
if (typeof desc === "string" && desc.length > 0) {
|
|
56
|
+
imageLabel = slugify(desc);
|
|
57
|
+
}
|
|
58
|
+
else if (name === "get_job_status" && typeof args.job_id === "string") {
|
|
59
|
+
// Look up the original description stored when the job was created
|
|
60
|
+
const jobDesc = getJobDescription(args.job_id);
|
|
61
|
+
if (jobDesc) {
|
|
62
|
+
imageLabel = slugify(jobDesc);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const endpoint = getJobEndpoint(args.job_id);
|
|
66
|
+
if (endpoint) {
|
|
67
|
+
imageLabel = endpoint.replace(/^\//, "").replace(/-/g, "_");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const { images, result: strippedResult } = extractAndSaveImages(result, imageLabel);
|
|
72
|
+
process.stderr.write(`[pixellab-forge-mcp] Extracted ${images.length} image(s), valid for inline: ${images.filter(i => i.base64.length > 0).length}\n`);
|
|
73
|
+
if (images.length > 0) {
|
|
74
|
+
const first = images[0];
|
|
75
|
+
process.stderr.write(`[pixellab-forge-mcp] First image: base64 length=${first.base64.length}, mime=${first.mimeType}, path=${first.filePath}\n`);
|
|
76
|
+
}
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (images.length > 0) {
|
|
79
|
+
// Limit inline image blocks to avoid overwhelming the Claude API.
|
|
80
|
+
// All images are still saved to disk regardless.
|
|
81
|
+
const MAX_INLINE_IMAGES = 4;
|
|
82
|
+
const inlineImages = images.slice(0, MAX_INLINE_IMAGES);
|
|
83
|
+
parts.push({
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `Saved ${images.length} image(s):\n${images.map((img) => img.filePath).join("\n")}${images.length > MAX_INLINE_IMAGES
|
|
86
|
+
? `\n\n(Showing first ${MAX_INLINE_IMAGES} of ${images.length} images inline. All images saved to disk.)`
|
|
87
|
+
: ""}`,
|
|
88
|
+
});
|
|
89
|
+
for (const img of inlineImages) {
|
|
90
|
+
if (img.base64) {
|
|
91
|
+
parts.push({
|
|
92
|
+
type: "image",
|
|
93
|
+
data: img.base64,
|
|
94
|
+
mimeType: img.mimeType,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
parts.push({
|
|
100
|
+
type: "text",
|
|
101
|
+
text: JSON.stringify(strippedResult, null, 2),
|
|
102
|
+
});
|
|
103
|
+
return { content: parts };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const promptMap = new Map(prompts.map((p) => [p.name, p]));
|
|
114
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
115
|
+
prompts: prompts.map((p) => ({
|
|
116
|
+
name: p.name,
|
|
117
|
+
description: p.description,
|
|
118
|
+
arguments: p.arguments,
|
|
119
|
+
})),
|
|
120
|
+
}));
|
|
121
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
122
|
+
const { name } = request.params;
|
|
123
|
+
const args = request.params.arguments ?? {};
|
|
124
|
+
const prompt = promptMap.get(name);
|
|
125
|
+
if (!prompt) {
|
|
126
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
description: prompt.description,
|
|
130
|
+
messages: prompt.messages(args),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
async function main() {
|
|
134
|
+
const transport = new StdioServerTransport();
|
|
135
|
+
await server.connect(transport);
|
|
136
|
+
}
|
|
137
|
+
main().catch((error) => {
|
|
138
|
+
console.error("Fatal error:", error);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface JobEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
status: "pending" | "completed" | "failed";
|
|
6
|
+
created: string;
|
|
7
|
+
completed?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function logJobStart(jobId: string, endpoint: string, description?: string): void;
|
|
10
|
+
export declare function logJobComplete(jobId: string): void;
|
|
11
|
+
export declare function logJobFailed(jobId: string): void;
|
|
12
|
+
export declare function getPendingJobs(): JobEntry[];
|
|
13
|
+
export declare function getJobLog(): JobEntry[];
|
|
14
|
+
export declare function getJobEndpoint(jobId: string): string | undefined;
|
|
15
|
+
export declare function getJobDescription(jobId: string): string | undefined;
|
|
16
|
+
export {};
|
package/dist/job-log.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
const LOG_DIR = join(tmpdir(), "pixellab-forge");
|
|
5
|
+
const LOG_FILE = join(LOG_DIR, "jobs.json");
|
|
6
|
+
const MAX_ENTRIES = 200;
|
|
7
|
+
const TTL_HOURS = 24;
|
|
8
|
+
function ensureDir() {
|
|
9
|
+
try {
|
|
10
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// already exists
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readLog() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(LOG_FILE, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeLog(entries) {
|
|
25
|
+
ensureDir();
|
|
26
|
+
writeFileSync(LOG_FILE, JSON.stringify(entries, null, 2));
|
|
27
|
+
}
|
|
28
|
+
/** Remove completed jobs older than TTL and cap total entries */
|
|
29
|
+
function prune(entries) {
|
|
30
|
+
const cutoff = Date.now() - TTL_HOURS * 60 * 60 * 1000;
|
|
31
|
+
const pruned = entries.filter((e) => {
|
|
32
|
+
// Always keep pending jobs (they might still be recoverable)
|
|
33
|
+
if (e.status === "pending")
|
|
34
|
+
return true;
|
|
35
|
+
// Remove completed/failed jobs older than TTL
|
|
36
|
+
const ts = new Date(e.completed ?? e.created).getTime();
|
|
37
|
+
return ts > cutoff;
|
|
38
|
+
});
|
|
39
|
+
// Cap at MAX_ENTRIES, keeping newest
|
|
40
|
+
if (pruned.length > MAX_ENTRIES) {
|
|
41
|
+
return pruned.slice(pruned.length - MAX_ENTRIES);
|
|
42
|
+
}
|
|
43
|
+
return pruned;
|
|
44
|
+
}
|
|
45
|
+
export function logJobStart(jobId, endpoint, description) {
|
|
46
|
+
const entries = prune(readLog());
|
|
47
|
+
entries.push({
|
|
48
|
+
id: jobId,
|
|
49
|
+
endpoint,
|
|
50
|
+
description,
|
|
51
|
+
status: "pending",
|
|
52
|
+
created: new Date().toISOString(),
|
|
53
|
+
});
|
|
54
|
+
writeLog(entries);
|
|
55
|
+
}
|
|
56
|
+
export function logJobComplete(jobId) {
|
|
57
|
+
const entries = readLog();
|
|
58
|
+
const entry = entries.find((e) => e.id === jobId);
|
|
59
|
+
if (entry) {
|
|
60
|
+
entry.status = "completed";
|
|
61
|
+
entry.completed = new Date().toISOString();
|
|
62
|
+
writeLog(prune(entries));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function logJobFailed(jobId) {
|
|
66
|
+
const entries = readLog();
|
|
67
|
+
const entry = entries.find((e) => e.id === jobId);
|
|
68
|
+
if (entry) {
|
|
69
|
+
entry.status = "failed";
|
|
70
|
+
entry.completed = new Date().toISOString();
|
|
71
|
+
writeLog(prune(entries));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function getPendingJobs() {
|
|
75
|
+
return readLog().filter((e) => e.status === "pending");
|
|
76
|
+
}
|
|
77
|
+
export function getJobLog() {
|
|
78
|
+
return prune(readLog());
|
|
79
|
+
}
|
|
80
|
+
export function getJobEndpoint(jobId) {
|
|
81
|
+
const entry = readLog().find((e) => e.id === jobId);
|
|
82
|
+
return entry?.endpoint;
|
|
83
|
+
}
|
|
84
|
+
export function getJobDescription(jobId) {
|
|
85
|
+
const entry = readLog().find((e) => e.id === jobId);
|
|
86
|
+
return entry?.description;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=job-log.js.map
|