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 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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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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 {};
@@ -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