@iborymagic/aseprite-mcp 0.2.0 → 0.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/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  This server automates Aseprite workflows using the Model Context Protocol (MCP).
3
3
  It enables AI, chat assistants, and automation pipelines to directly execute Aseprite tasks such as sprite sheet export, frame extraction, and metadata output.
4
4
 
5
- *Lua-based automation and high-level sprite/tile generation features are not included yet.
6
5
  *Aseprite must be installed in order to use this MCP server.
6
+ *Currently, only character creation is possible, and not with images created through Aseprite.
7
7
 
8
8
  ## Features Overview
9
9
  ### V1 - Export/Utility
@@ -18,15 +18,27 @@ Adds deeper control using Aseprite Lua scripting, enabling safe AI-driven editin
18
18
  - `aseprite_list_lua_templates`: Lists available Lua templates
19
19
  - `aseprite_run_lua_template`: Runs a predefined safe Lua automation(templates)
20
20
  - `remove_layer_by_name`: Removing specific layers
21
- - `export_tag_frames`: Palette recoloring
22
- - `recolor_palette`: Normalizing animation speed
23
- - `normalize_animation_speed`: Exporting only specific animation tags
21
+ - `recolor_palette`: Palette recoloring
22
+ - `normalize_animation_speed`: Normalizing animation speed
24
23
  - `auto_crop_transparent`: Removing empty transparent borders around the sprite
25
24
  - `export_layer_only`: Exporting only a specific layer as a flattened PNG image
26
25
  - `export_tag_frames`: Exporting all frames within a specific animation tag as individual PNG files
27
26
  - `merge_visible_layers`: Merging all currently visible layers into a single layer
28
27
  - `aseprite_run_lua`: Executes a raw Lua script (advanced / unsafe)
29
28
 
29
+ ### V3 - Image Generation
30
+ (+ Character Pipeline)
31
+ - Adds connection with LLM and Image Generation AI, enabling import/export image in Aseprite.
32
+ - Only available on Character Generation
33
+ - `character_generate_concept`: Generating concept image using Generative AI
34
+ - `character_import_from_concept`: Importing character concept from concept image
35
+ - `character_generate_full`: Execute whole character generating pipeline
36
+ - `character_pipeline_analyze`: Analyze character concept image
37
+ - `character_pipeline_normalize`: Normalize your animations with fixed frames, crops, tags, etc.
38
+ - `character_pipeline_export`: Export animation in png + json format
39
+
40
+ *You can change Image Generation AI by implementing ImageGenerator class in image-generator.ts
41
+
30
42
  ## How to use
31
43
  1) Run directly with npx
32
44
  ```bash
@@ -40,11 +52,11 @@ npm run build
40
52
  npx aseprite-mcp
41
53
  ```
42
54
 
43
- ### Using with ChatGPT
44
- Add the following to your mcp.json
55
+ ### Using with Claude Desktop
56
+ Add the following to your claude_desktop_config.json
45
57
  ```json
46
58
  {
47
- "servers": {
59
+ "mcpServers": {
48
60
  "aseprite-mcp": {
49
61
  "command": "npx",
50
62
  "args": ["-y", "aseprite-mcp"]
@@ -53,19 +65,8 @@ Add the following to your mcp.json
53
65
  }
54
66
  ```
55
67
 
56
- ### Using with Claude
57
- Add the following to your servers.json
58
- ```json
59
- {
60
- "aseprite-mcp": {
61
- "command": "npx",
62
- "args": ["-y", "aseprite-mcp"]
63
- }
64
- }
65
- ```
66
-
67
68
  ### Using with Cursor
68
- Add the following to your .cursor.json
69
+ Add the following to your mcp.json
69
70
  ```json
70
71
  {
71
72
  "mcpServers": {
@@ -2,14 +2,22 @@ import { exec } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { resolveAsepritePath } from "./env.js";
4
4
  const execAsync = promisify(exec);
5
- export async function runAsepriteCommand(args) {
5
+ export async function runAsepriteCommand(args, timeout = 10_000) {
6
6
  const path = await resolveAsepritePath();
7
7
  const command = `"${path}" ${args.join(" ")}`;
8
8
  try {
9
- const { stdout, stderr } = await execAsync(command);
10
- return { command, stdout, stderr };
9
+ const { stdout, stderr } = await execAsync(command, { timeout });
10
+ return { command, stdout, stderr, timedOut: false };
11
11
  }
12
12
  catch (error) {
13
+ if (error.killed === true && error.code === null) {
14
+ return {
15
+ command,
16
+ stdout: "",
17
+ stderr: "Aseprite command timed out",
18
+ timedOut: true
19
+ };
20
+ }
13
21
  throw new Error(`Failed to run Aseprite command: ${command}\n${error instanceof Error ? error.message : String(error)}`);
14
22
  }
15
23
  }
@@ -135,11 +135,5 @@ export function createToolSchemas() {
135
135
  dataFile: z.string(),
136
136
  format: z.string().optional(),
137
137
  }),
138
- aseprite_output_result: z.object({
139
- content: z.array(z.object({
140
- type: z.literal("text"),
141
- text: z.string(),
142
- })),
143
- }),
144
138
  };
145
139
  }
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import axios from "axios";
4
+ export class StubImageGenerator {
5
+ async generate(params) {
6
+ throw new Error(`Image generator not configured. Tried to generate: "${params.prompt}" to ${params.outputPath}`);
7
+ }
8
+ }
9
+ export class StableDiffusionWebuiGenerator {
10
+ baseUrl;
11
+ constructor(baseUrl) {
12
+ this.baseUrl = baseUrl;
13
+ }
14
+ async generate(params) {
15
+ const url = `${this.baseUrl}/sdapi/v1/txt2img`;
16
+ const prompt = `${params.prompt}, ` +
17
+ `pixel art, retro 16-bit, clean outline, sharp edges, high contrast, game sprite`;
18
+ const negative = params.negativePrompt ??
19
+ "photo, realistic, blurry, lowres, smear, oil painting";
20
+ const body = {
21
+ prompt,
22
+ negative_prompt: negative,
23
+ width: params.width,
24
+ height: params.height,
25
+ steps: 28,
26
+ sampler_name: "DPM++ 2M",
27
+ cfg_scale: 7,
28
+ seed: params.seed ?? -1,
29
+ restore_faces: false,
30
+ tiling: false
31
+ };
32
+ const res = await axios.post(url, body, {
33
+ timeout: 1000 * 60 * 3
34
+ });
35
+ if (!res.data?.images?.length) {
36
+ throw new Error("Stable Diffusion returned no images");
37
+ }
38
+ const imgBase64 = res.data.images[0];
39
+ const buffer = Buffer.from(imgBase64, "base64");
40
+ const dir = path.dirname(params.outputPath);
41
+ await fs.mkdir(dir, { recursive: true });
42
+ await fs.writeFile(params.outputPath, buffer);
43
+ let seed = params.seed;
44
+ try {
45
+ const info = JSON.parse(res.data.info ?? "{}");
46
+ if (info.seed)
47
+ seed = info.seed;
48
+ }
49
+ catch { }
50
+ return {
51
+ imagePath: params.outputPath,
52
+ seed
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,210 @@
1
+ import z from "zod";
2
+ import { errorResult, successResult } from "../util.js";
3
+ import { ensureSafePath } from "../aseprite/path.js";
4
+ import path from "path";
5
+ import fs from "node:fs/promises";
6
+ import { runLuaScriptFile } from "../lua/cli.js";
7
+ import { createToolHandlers as createPipelineToolHandlers } from "../pipeline/tools.js";
8
+ import { fileURLToPath } from "node:url";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const toolSchemas = createToolSchemas();
12
+ export function createToolHandlers() {
13
+ const character_generate_concept = async ({ params: { prompt, workspaceDir, fileName = "concept.png", width = 256, height = 256, seed, stylePreset = "pixel_art" } }, generator) => {
14
+ try {
15
+ const workspaceAbs = ensureSafePath(workspaceDir, { mustExist: false });
16
+ await fs.mkdir(workspaceAbs, { recursive: true });
17
+ const outputPath = path.join(workspaceAbs, fileName);
18
+ const result = await generator.generate({
19
+ prompt,
20
+ negativePrompt: undefined,
21
+ width,
22
+ height,
23
+ seed,
24
+ stylePreset,
25
+ outputPath
26
+ });
27
+ return successResult("character_generate_concept", {
28
+ prompt,
29
+ workspaceDir: workspaceAbs,
30
+ imagePath: result.imagePath,
31
+ seed: result.seed
32
+ });
33
+ }
34
+ catch (err) {
35
+ return errorResult("character_generate_concept", `Character generate concept failed: ${err instanceof Error ? err.message : String(err)}`);
36
+ }
37
+ };
38
+ const character_import_from_concept = async ({ params: { conceptImage, outputFile, spriteSize: spriteSizeParam = 0, animationSpec: animationSpecParam = "Idle:4" }, }) => {
39
+ try {
40
+ const conceptAbs = ensureSafePath(conceptImage, {
41
+ mustExist: true
42
+ });
43
+ const outputAbs = ensureSafePath(outputFile, {
44
+ mustExist: false
45
+ });
46
+ const spriteSize = spriteSizeParam ?? 0;
47
+ const animationSpec = animationSpecParam ?? "Idle:4";
48
+ // Lua templates are in the source directory, reference from project root
49
+ const luaScriptPath = path.join(process.cwd(), "src", "lua", "templates", "character_import_from_concept.lua");
50
+ const result = await runLuaScriptFile(luaScriptPath, {
51
+ conceptImage: conceptAbs,
52
+ outputFile: outputAbs,
53
+ spriteSize,
54
+ animationSpec
55
+ });
56
+ if (result.timedOut) {
57
+ return errorResult("character_import_from_concept", `Lua script timed out while importing from concept: ${luaScriptPath}`);
58
+ }
59
+ return successResult("character_import_from_concept", {
60
+ command: result.command,
61
+ conceptImage: conceptAbs,
62
+ outputFile: outputAbs,
63
+ spriteSize,
64
+ animationSpec
65
+ });
66
+ }
67
+ catch (err) {
68
+ return errorResult("character_import_from_concept", `Character import from concept failed: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ };
71
+ const character_generate_full = async ({ params: { prompt, config, paths } }, generator) => {
72
+ try {
73
+ const spriteSize = config?.spriteSize ?? 32;
74
+ const animations = config?.animations ?? [
75
+ { name: "Idle", frames: 4 },
76
+ { name: "Walk", frames: 8 }
77
+ ];
78
+ const seed = config?.seed;
79
+ const charName = (paths.name ??
80
+ prompt
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9]+/g, "_")
83
+ .replace(/^_+|_+$/g, "")
84
+ .slice(0, 32)) ||
85
+ "character";
86
+ const workspaceBase = ensureSafePath(paths.workspaceDir, {
87
+ mustExist: false
88
+ });
89
+ const exportBase = ensureSafePath(paths.exportDir, {
90
+ mustExist: false
91
+ });
92
+ const workspaceDir = path.join(workspaceBase, charName);
93
+ const exportDir = path.join(exportBase, charName);
94
+ await fs.mkdir(workspaceDir, { recursive: true });
95
+ await fs.mkdir(exportDir, { recursive: true });
96
+ const conceptResult = await character_generate_concept({
97
+ params: {
98
+ prompt,
99
+ workspaceDir,
100
+ fileName: "concept.png",
101
+ width: spriteSize * 8,
102
+ height: spriteSize * 8,
103
+ seed,
104
+ stylePreset: "pixel_art"
105
+ }
106
+ }, generator);
107
+ const conceptContent = conceptResult.content?.[0];
108
+ const conceptJson = conceptContent
109
+ ? JSON.parse(conceptContent.text)
110
+ : null;
111
+ if (!conceptJson || !conceptJson.success) {
112
+ return errorResult("character_generate_full", {
113
+ stage: "generate_concept",
114
+ error: conceptJson?.error ?? "Concept generation failed"
115
+ });
116
+ }
117
+ const conceptImage = conceptJson.result.imagePath;
118
+ const animationSpec = animations
119
+ .map((a) => `${a.name}:${a.frames}`)
120
+ .join(",");
121
+ const outputFile = path.join(workspaceDir, `${charName}.aseprite`);
122
+ const importResult = await character_import_from_concept({
123
+ params: {
124
+ conceptImage,
125
+ outputFile,
126
+ spriteSize,
127
+ animationSpec
128
+ }
129
+ }, {});
130
+ const importContent = importResult.content?.[0];
131
+ const importJson = importContent ? JSON.parse(importContent.text) : null;
132
+ if (!importJson || !importJson.success) {
133
+ return errorResult("character_generate_full", {
134
+ stage: "import_sprite",
135
+ error: importJson?.error ?? "Sprite import failed"
136
+ });
137
+ }
138
+ const asepriteFile = importJson.result.outputFile;
139
+ const pipelineToolHandlers = createPipelineToolHandlers();
140
+ const buildResult = await pipelineToolHandlers.character_pipeline_build({
141
+ inputFile: asepriteFile,
142
+ exportDir
143
+ }, {});
144
+ const buildContent = buildResult.content?.[0];
145
+ const buildJson = buildContent ? JSON.parse(buildContent.text) : null;
146
+ if (!buildJson || !buildJson.success) {
147
+ return errorResult("character_generate_full", {
148
+ stage: "pipeline_build",
149
+ error: buildJson?.error ?? "Character pipeline build failed"
150
+ });
151
+ }
152
+ return successResult("character_generate_full", {
153
+ name: charName,
154
+ prompt,
155
+ spriteSize,
156
+ workspaceDir,
157
+ exportDir,
158
+ concept: conceptJson.result,
159
+ asepriteFile,
160
+ pipeline: buildJson.result
161
+ });
162
+ }
163
+ catch (err) {
164
+ return errorResult("character_generate_full", err instanceof Error ? err : new Error(String(err)));
165
+ }
166
+ };
167
+ return {
168
+ character_generate_concept: async (params, generator) => character_generate_concept(params, generator),
169
+ character_import_from_concept,
170
+ character_generate_full: async (params, generator) => character_generate_full(params, generator),
171
+ };
172
+ }
173
+ export function createToolSchemas() {
174
+ return {
175
+ character_generate_concept: z.object({
176
+ params: z.object({
177
+ prompt: z.string(),
178
+ workspaceDir: z.string(),
179
+ fileName: z.string().optional(),
180
+ width: z.number().optional(),
181
+ height: z.number().optional(),
182
+ seed: z.number().optional(),
183
+ stylePreset: z.string().optional(),
184
+ }),
185
+ }),
186
+ character_import_from_concept: z.object({
187
+ params: z.object({
188
+ conceptImage: z.string(),
189
+ outputFile: z.string(),
190
+ spriteSize: z.number().optional(),
191
+ animationSpec: z.string().optional(),
192
+ }),
193
+ }),
194
+ character_generate_full: z.object({
195
+ params: z.object({
196
+ prompt: z.string(),
197
+ config: z.object({
198
+ spriteSize: z.number().optional(),
199
+ animations: z.array(z.object({ name: z.string(), frames: z.number() })).optional(),
200
+ seed: z.number().optional(),
201
+ }),
202
+ paths: z.object({
203
+ workspaceDir: z.string(),
204
+ exportDir: z.string(),
205
+ name: z.string().optional(),
206
+ }),
207
+ }),
208
+ }),
209
+ };
210
+ }
package/build/index.js CHANGED
@@ -3,53 +3,75 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { createToolHandlers as createAsepriteToolHandlers, createToolSchemas as createAsepriteToolSchemas } from "./aseprite/tools.js";
5
5
  import { createToolHandlers as createLuaToolHandlers, createToolSchemas as createLuaToolSchemas } from "./lua/tools.js";
6
+ import { createToolHandlers as createCharacterPipelineToolHandlers, createToolSchemas as createCharacterPipelineToolSchemas } from "./pipeline/tools.js";
7
+ import { createToolHandlers as createGeneratorToolHandlers, createToolSchemas as createGeneratorToolSchemas } from "./generator/tools.js";
8
+ import { StableDiffusionWebuiGenerator } from "./generator/image-generator.js";
6
9
  const server = new McpServer({
7
10
  name: "aseprite-mcp",
8
11
  version: "0.1.0"
9
12
  });
10
13
  const asepriteToolSchemas = createAsepriteToolSchemas();
11
14
  const luaToolSchemas = createLuaToolSchemas();
15
+ const characterPipelineToolSchemas = createCharacterPipelineToolSchemas();
16
+ const generatorToolSchemas = createGeneratorToolSchemas();
12
17
  const asepriteToolHandlers = createAsepriteToolHandlers();
13
18
  const luaToolHandlers = createLuaToolHandlers();
14
- server.registerTool("aseprite_check_environment", {
15
- description: "Check the environment of Aseprite",
16
- inputSchema: undefined,
17
- outputSchema: undefined,
18
- }, asepriteToolHandlers.aseprite_check_environment);
19
+ const characterPipelineToolHandlers = createCharacterPipelineToolHandlers();
20
+ const generatorToolHandlers = createGeneratorToolHandlers();
21
+ server.registerTool("aseprite_check_environment", { description: "Check the environment of Aseprite" }, asepriteToolHandlers.aseprite_check_environment);
19
22
  server.registerTool("aseprite_export_sheet", {
20
23
  description: "Export Aseprite file to sprite sheet image",
21
24
  inputSchema: asepriteToolSchemas.aseprite_export_sheet,
22
- outputSchema: asepriteToolSchemas.aseprite_output_result,
23
25
  }, asepriteToolHandlers.aseprite_export_sheet);
24
26
  server.registerTool("aseprite_export_frames", {
25
27
  description: "Export each frame of Aseprite file",
26
28
  inputSchema: asepriteToolSchemas.aseprite_export_frames,
27
- outputSchema: asepriteToolSchemas.aseprite_output_result,
28
29
  }, asepriteToolHandlers.aseprite_export_frames);
29
30
  server.registerTool("aseprite_export_metadata", {
30
31
  description: "Export metadata json from Aseprite file",
31
32
  inputSchema: asepriteToolSchemas.aseprite_export_metadata,
32
- outputSchema: asepriteToolSchemas.aseprite_output_result,
33
33
  }, asepriteToolHandlers.aseprite_export_metadata);
34
- server.registerTool("aseprite_list_lua_templates", {
35
- description: "List available Aseprite Lua templates.",
36
- inputSchema: undefined,
37
- outputSchema: undefined,
38
- }, luaToolHandlers.aseprite_list_lua_templates);
34
+ server.registerTool("aseprite_list_lua_templates", { description: "List available Aseprite Lua templates." }, luaToolHandlers.aseprite_list_lua_templates);
39
35
  server.registerTool("aseprite_run_lua_template", {
40
36
  description: "Run a predefined Aseprite Lua template with parameters.",
41
37
  inputSchema: luaToolSchemas.aseprite_run_lua_template,
42
- outputSchema: luaToolSchemas.lua_output_result,
43
38
  }, luaToolHandlers.aseprite_run_lua_template);
44
39
  server.registerTool("aseprite_run_lua_script", {
45
40
  description: "Run a raw Lua script (advanced / unsafe).",
46
41
  inputSchema: luaToolSchemas.aseprite_run_lua_script,
47
- outputSchema: luaToolSchemas.lua_output_result,
48
42
  }, luaToolHandlers.aseprite_run_lua_script);
43
+ server.registerTool("character_pipeline_analyze", {
44
+ description: "Analyze a character sprite",
45
+ inputSchema: characterPipelineToolSchemas.character_pipeline_analyze,
46
+ }, characterPipelineToolHandlers.character_pipeline_analyze);
47
+ server.registerTool("character_pipeline_normalize", {
48
+ description: "Normalize a character sprite",
49
+ inputSchema: characterPipelineToolSchemas.character_pipeline_normalize,
50
+ }, characterPipelineToolHandlers.character_pipeline_normalize);
51
+ server.registerTool("character_pipeline_export", {
52
+ description: "Export a character sprite",
53
+ inputSchema: characterPipelineToolSchemas.character_pipeline_export,
54
+ }, characterPipelineToolHandlers.character_pipeline_export);
55
+ server.registerTool("character_pipeline_build", {
56
+ description: "Build a character sprite",
57
+ inputSchema: characterPipelineToolSchemas.character_pipeline_build,
58
+ }, characterPipelineToolHandlers.character_pipeline_build);
59
+ const generator = new StableDiffusionWebuiGenerator("http://127.0.0.1:7860");
60
+ server.registerTool("character_generate_concept", {
61
+ description: "Generate a character concept",
62
+ inputSchema: generatorToolSchemas.character_generate_concept,
63
+ }, async (params) => generatorToolHandlers.character_generate_concept(params, generator));
64
+ server.registerTool("character_import_from_concept", {
65
+ description: "Import a character from a concept",
66
+ inputSchema: generatorToolSchemas.character_import_from_concept,
67
+ }, generatorToolHandlers.character_import_from_concept);
68
+ server.registerTool("character_generate_full", {
69
+ description: "Generate a full character",
70
+ inputSchema: generatorToolSchemas.character_generate_full,
71
+ }, async (params) => generatorToolHandlers.character_generate_full(params, generator));
49
72
  async function main() {
50
73
  const transport = new StdioServerTransport();
51
74
  await server.connect(transport);
52
- console.log("Aseprite MCP server started");
53
75
  }
54
76
  main().catch(err => {
55
77
  console.error("MCP Error:", err);
@@ -0,0 +1,16 @@
1
+ import { runAsepriteCommand } from "../aseprite/cli.js";
2
+ import { ensureSafePath } from "../aseprite/path.js";
3
+ export async function runLuaScriptFile(scriptPath, params) {
4
+ const args = ["--batch"];
5
+ if (typeof params.inputFile === "string") {
6
+ const inputAbs = ensureSafePath(params.inputFile, { mustExist: true });
7
+ args.push(`"${inputAbs}"`);
8
+ }
9
+ for (const [key, value] of Object.entries(params)) {
10
+ if (key === "inputFile" || value == null)
11
+ continue;
12
+ args.push("--script-param", `${key}="${String(value)}"`);
13
+ }
14
+ args.push("--script", `"${scriptPath}"`);
15
+ return runAsepriteCommand(args);
16
+ }
@@ -0,0 +1,79 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ const __filename = fileURLToPath(import.meta.url);
4
+ const __dirname = path.dirname(__filename);
5
+ export const LUA_TEMPLATES = [
6
+ {
7
+ id: "remove_layer_by_name",
8
+ description: "Removes a layer with a given name and saves to a new file (or overwrites).",
9
+ params: ["inputFile", "layerName"],
10
+ optionalParams: ["saveOutput"],
11
+ scriptPath: path.join(__dirname, "templates", "remove_layer_by_name.lua")
12
+ },
13
+ {
14
+ id: "export_tag_frames",
15
+ description: "Exports only the frames of a specific tag as PNG images.",
16
+ params: ["inputFile", "tag", "outputDir"],
17
+ optionalParams: ["filenamePrefix"],
18
+ scriptPath: path.join(__dirname, "templates", "export_tag_frames.lua")
19
+ },
20
+ {
21
+ id: "recolor_palette",
22
+ description: "Recolors the palette based on a mapping of from->to colors.",
23
+ params: ["inputFile", "saveOutput", "mapping"],
24
+ scriptPath: path.join(__dirname, "templates", "recolor_palette.lua")
25
+ },
26
+ {
27
+ id: "normalize_animation_speed",
28
+ description: "Normalizes all frame durations to a single target duration (in seconds).",
29
+ params: ["inputFile", "saveOutput", "targetDuration"],
30
+ scriptPath: path.join(__dirname, "templates", "normalize_animation_speed.lua")
31
+ },
32
+ {
33
+ id: "auto_crop_transparent",
34
+ description: "Automatically crops empty transparent borders of the sprite",
35
+ params: ["inputFile", "saveOutput"],
36
+ scriptPath: path.join(__dirname, "templates", "auto_crop_transparent.lua")
37
+ },
38
+ {
39
+ id: "merge_visible_layers",
40
+ description: "Merges currently visible layers and saves resulting flattened sprite",
41
+ params: ["inputFile", "saveOutput"],
42
+ scriptPath: path.join(__dirname, "templates", "merge_visible_layers.lua")
43
+ },
44
+ {
45
+ id: "export_layer_only",
46
+ description: "Exports only the specified layer to a flattened PNG",
47
+ params: ["inputFile", "layerName", "outputDir"],
48
+ scriptPath: path.join(__dirname, "templates", "export_layer_only.lua")
49
+ },
50
+ {
51
+ id: "export_tag_frames",
52
+ description: "Exports frames inside a specific animation tag to PNG files",
53
+ params: ["inputFile", "tag", "outputDir"],
54
+ optionalParams: ["filenamePrefix"],
55
+ scriptPath: path.join(__dirname, "templates", "export_tag_frames.lua")
56
+ },
57
+ {
58
+ id: "character_normalize",
59
+ description: "Normalizes frame durations inside all tags to a single target duration (in seconds).",
60
+ params: ["inputFile", "saveOutput", "targetMs"],
61
+ optionalParams: ["autoCrop"],
62
+ scriptPath: path.join(__dirname, "templates", "character_normalize.lua")
63
+ },
64
+ {
65
+ id: "character_export",
66
+ description: "Exports a character sprite",
67
+ params: ["inputFile", "exportDir", "sheetType", "format"],
68
+ scriptPath: path.join(__dirname, "templates", "character_export.lua")
69
+ },
70
+ {
71
+ id: "character_build",
72
+ description: "Builds a character sprite",
73
+ params: ["inputFile", "tempOutput", "exportDir", "normalizeOption", "exportOption"],
74
+ scriptPath: path.join(__dirname, "templates", "character_build.lua")
75
+ }
76
+ ];
77
+ export function findLuaTemplate(id) {
78
+ return LUA_TEMPLATES.find(t => t.id === id);
79
+ }
@@ -1,5 +1,6 @@
1
1
  import { errorResult, successResult } from "../util.js";
2
- import { findLuaTemplate, LUA_TEMPLATES, runLuaScript, runLuaTemplate } from "./templates.js";
2
+ import { findLuaTemplate, LUA_TEMPLATES } from "./template.js";
3
+ import { runLuaScriptFile } from "./cli.js";
3
4
  import { ensureSafePath } from "../aseprite/path.js";
4
5
  import path from "node:path";
5
6
  import z from "zod";
@@ -27,16 +28,27 @@ export function createToolHandlers() {
27
28
  return errorResult("aseprite_run_lua_template", new Error(`Missing required params: ${missing.join(", ")}`));
28
29
  }
29
30
  try {
30
- const result = await runLuaTemplate(template, params);
31
+ const result = await runLuaScriptFile(template.scriptPath, params);
32
+ if (result.timedOut) {
33
+ return errorResult("aseprite_run_lua_template", `Lua script timed out while executing template: ${templateId}`);
34
+ }
35
+ const stderrTrimmed = result.stderr.trim();
36
+ const stdoutTrimmed = result.stdout.trim();
37
+ if (stderrTrimmed && stderrTrimmed.includes("ERROR:")) {
38
+ return errorResult("aseprite_run_lua_template", `Script execution failed: ${stderrTrimmed}`);
39
+ }
40
+ if (stdoutTrimmed && stdoutTrimmed.includes("ERROR:")) {
41
+ return errorResult("aseprite_run_lua_template", `Script execution failed: ${stdoutTrimmed}`);
42
+ }
31
43
  return successResult("aseprite_run_lua_template", {
32
44
  command: result.command,
33
45
  templateId,
34
- stdout: result.stdout.trim(),
35
- stderr: result.stderr.trim()
46
+ stdout: stdoutTrimmed,
47
+ stderr: stderrTrimmed
36
48
  });
37
49
  }
38
50
  catch (err) {
39
- return errorResult("aseprite_run_lua_template", new Error(`Execution failed: ${err instanceof Error ? err.message : String(err)}`));
51
+ return errorResult("aseprite_run_lua_template", `Execution failed: ${err instanceof Error ? err.message : String(err)}`);
40
52
  }
41
53
  };
42
54
  const aseprite_run_lua_script = async ({ scriptPath, scriptContent, params = {} }) => {
@@ -50,15 +62,26 @@ export function createToolHandlers() {
50
62
  await fs.writeFile(tempPath, String(scriptContent), "utf8");
51
63
  luaFilePath = tempPath;
52
64
  }
53
- const result = await runLuaScript(luaFilePath, params);
65
+ const result = await runLuaScriptFile(luaFilePath, params);
66
+ if (result.timedOut) {
67
+ return errorResult("aseprite_run_lua_script", `Lua script timed out while executing script: ${luaFilePath}`);
68
+ }
69
+ const stderrTrimmed = result.stderr.trim();
70
+ const stdoutTrimmed = result.stdout.trim();
71
+ if (stderrTrimmed && stderrTrimmed.includes("ERROR:")) {
72
+ return errorResult("aseprite_run_lua_script", `Script execution failed: ${stderrTrimmed}`);
73
+ }
74
+ if (stdoutTrimmed && stdoutTrimmed.includes("ERROR:")) {
75
+ return errorResult("aseprite_run_lua_script", `Script execution failed: ${stdoutTrimmed}`);
76
+ }
54
77
  return successResult("aseprite_run_lua_script", {
55
78
  command: result.command,
56
- stdout: result.stdout.trim(),
57
- stderr: result.stderr.trim()
79
+ stdout: stdoutTrimmed,
80
+ stderr: stderrTrimmed
58
81
  });
59
82
  }
60
83
  catch (err) {
61
- return errorResult("aseprite_run_lua_script", new Error(`Execution failed: ${err instanceof Error ? err.message : String(err)}`));
84
+ return errorResult("aseprite_run_lua_script", `Execution failed: ${err instanceof Error ? err.message : String(err)}`);
62
85
  }
63
86
  };
64
87
  return {
@@ -83,11 +106,5 @@ export function createToolSchemas() {
83
106
  .refine(v => !!v.scriptPath || !!v.scriptContent, {
84
107
  message: "Either scriptPath or scriptContent is required."
85
108
  }),
86
- lua_output_result: z.object({
87
- content: z.array(z.object({
88
- type: z.literal("text"),
89
- text: z.string()
90
- }))
91
- })
92
109
  };
93
110
  }
@@ -0,0 +1,66 @@
1
+ export function analyzeCharacterFromMetadata(inputFile, metaJson) {
2
+ const framesArray = Object.values(metaJson.frames);
3
+ const totalFrames = framesArray.length;
4
+ const width = metaJson.meta.size?.w ?? 0;
5
+ const height = metaJson.meta.size?.h ?? 0;
6
+ const tags = metaJson.meta.frameTags ?? [];
7
+ const tagAnalyses = [];
8
+ const warnings = [];
9
+ const recommendations = [];
10
+ const recommendedTags = ["Idle", "Walk", "Attack"];
11
+ for (const tag of tags) {
12
+ const { name, from, to } = tag;
13
+ const durationPattern = [];
14
+ const issues = [];
15
+ for (let i = from; i <= to; i++) {
16
+ const frame = framesArray[i];
17
+ if (frame) {
18
+ durationPattern.push(frame.duration);
19
+ }
20
+ else {
21
+ issues.push(`missing_frame_index_${i}`);
22
+ }
23
+ }
24
+ if (durationPattern.length > 1) {
25
+ const first = durationPattern[0];
26
+ const inconsistent = durationPattern.some((duration) => duration !== first);
27
+ if (inconsistent) {
28
+ issues.push("duration_inconsistent");
29
+ }
30
+ }
31
+ tagAnalyses.push({
32
+ name,
33
+ frames: to - from + 1,
34
+ from,
35
+ to,
36
+ durationPattern,
37
+ issues
38
+ });
39
+ }
40
+ const existingTagNames = new Set(tags.map((tag) => tag.name));
41
+ for (const tag of recommendedTags) {
42
+ const exists = Array.from(existingTagNames).some((tagName) => tagName.toLowerCase() === tag.toLowerCase());
43
+ if (!exists) {
44
+ warnings.push(`Missing recommended animation tag: ${tag}`);
45
+ }
46
+ }
47
+ if (tags.length === 0) {
48
+ warnings.push("No frame tags found. Consider defining Idle/Walk/Attack tags.");
49
+ recommendations.push("Define basic animation tags like Idle, Walk, and Attack.");
50
+ }
51
+ if (tagAnalyses.some((tagAnalysis) => tagAnalysis.issues.includes("duration_inconsistent"))) {
52
+ warnings.push("Some tags have inconsistent frame durations.");
53
+ recommendations.push("Normalize frame durations for smoother animations.");
54
+ }
55
+ return {
56
+ file: inputFile,
57
+ sprite: {
58
+ width,
59
+ height,
60
+ frames: totalFrames
61
+ },
62
+ tags: tagAnalyses,
63
+ warnings,
64
+ recommendations
65
+ };
66
+ }
@@ -0,0 +1,226 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ import z from "zod";
6
+ import { errorResult, successResult } from "../util.js";
7
+ import { analyzeCharacterFromMetadata } from "./character.js";
8
+ import { ensureSafePath } from "../aseprite/path.js";
9
+ import { runAsepriteCommand } from "../aseprite/cli.js";
10
+ import { runLuaScriptFile } from "../lua/cli.js";
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const toolSchemas = createToolSchemas();
14
+ export function createToolHandlers() {
15
+ const character_pipeline_analyze = async ({ inputFile }) => {
16
+ try {
17
+ const inputAbs = ensureSafePath(inputFile, { mustExist: true });
18
+ const tempJsonPath = path.join(os.tmpdir(), `${inputFile}-analyze-${Date.now()}.json`);
19
+ const args = [
20
+ "--batch",
21
+ `"${inputAbs}"`,
22
+ "--data",
23
+ `"${tempJsonPath}"`,
24
+ "--list-tags",
25
+ "--list-layers"
26
+ ];
27
+ const result = await runAsepriteCommand(args);
28
+ const metaJson = await fs.readFile(tempJsonPath, "utf8");
29
+ const parsedMeta = JSON.parse(metaJson);
30
+ const analysis = analyzeCharacterFromMetadata(inputAbs, parsedMeta);
31
+ return successResult("character_pipeline_analyze", {
32
+ command: result.command,
33
+ inputFile: inputAbs,
34
+ analysis,
35
+ stdout: result.stdout.trim(),
36
+ stderr: result.stderr.trim(),
37
+ });
38
+ }
39
+ catch (err) {
40
+ return errorResult("character_pipeline_analyze", `Character analysis failed: ${err instanceof Error ? err.message : String(err)}`);
41
+ }
42
+ };
43
+ const character_pipeline_normalize = async ({ inputFile, saveOutput, targetMs, autoCrop }) => {
44
+ try {
45
+ const inputAbs = ensureSafePath(inputFile, { mustExist: true });
46
+ const baseDir = path.dirname(inputAbs);
47
+ const baseName = path.basename(inputAbs, path.extname(inputAbs));
48
+ const outputPath = saveOutput ??
49
+ path.join(baseDir, `${baseName}_normalized.aseprite`);
50
+ const outputAbs = ensureSafePath(outputPath, { mustExist: false });
51
+ const targetDuration = targetMs ?? 100;
52
+ const autoCropEnabled = autoCrop ?? true;
53
+ // Lua templates are in the source directory, reference from project root
54
+ const luaScriptPath = path.join(process.cwd(), "src", "lua", "templates", "character_normalize.lua");
55
+ const result = await runLuaScriptFile(luaScriptPath, {
56
+ inputFile: inputAbs,
57
+ saveOutput: outputAbs,
58
+ targetMs: targetDuration,
59
+ autoCrop: autoCropEnabled
60
+ });
61
+ if (result.timedOut) {
62
+ return errorResult("character_pipeline_normalize", "Lua script timed out while normalizing character");
63
+ }
64
+ return successResult("character_pipeline_normalize", {
65
+ command: result.command,
66
+ inputFile: inputAbs,
67
+ outputFile: outputAbs,
68
+ targetMs: targetDuration,
69
+ autoCrop: autoCropEnabled,
70
+ stdout: result.stdout.trim(),
71
+ stderr: result.stderr.trim(),
72
+ });
73
+ }
74
+ catch (err) {
75
+ return errorResult("character_pipeline_normalize", `Character normalization failed: ${err instanceof Error ? err.message : String(err)}`);
76
+ }
77
+ };
78
+ const character_pipeline_export = async ({ inputFile, exportDir, sheetType = "packed", format = "json-hash" }) => {
79
+ try {
80
+ const inputAbs = ensureSafePath(inputFile, { mustExist: true });
81
+ const exportDirAbs = ensureSafePath(exportDir, { mustExist: false });
82
+ await fs.mkdir(exportDirAbs, { recursive: true });
83
+ const tempJsonPath = path.join(os.tmpdir(), `${inputFile}-export-${Date.now()}.json`);
84
+ const metaArgs = [
85
+ "--batch",
86
+ `"${inputAbs}"`,
87
+ "--data",
88
+ `"${tempJsonPath}"`,
89
+ "--list-tags"
90
+ ];
91
+ await runAsepriteCommand(metaArgs);
92
+ const metaJson = await fs.readFile(tempJsonPath, "utf8");
93
+ const parsedMeta = JSON.parse(metaJson);
94
+ const tags = parsedMeta.meta.frameTags ?? [];
95
+ if (tags.length === 0) {
96
+ return errorResult("character_pipeline_export", "No tags found in sprite. Define animation tags before export.");
97
+ }
98
+ const baseName = path.basename(inputAbs, path.extname(inputAbs));
99
+ const generated = [];
100
+ for (const tag of tags) {
101
+ const tagName = tag.name;
102
+ const safeTag = tagName.toLowerCase();
103
+ const pngPath = path.join(exportDirAbs, `${baseName}_${safeTag}.png`);
104
+ const jsonPath = path.join(exportDirAbs, `${baseName}_${safeTag}.json`);
105
+ const args = [
106
+ "--batch",
107
+ `"${inputAbs}"`,
108
+ "--tag",
109
+ `"${tagName}"`,
110
+ "--sheet",
111
+ `"${pngPath}"`,
112
+ "--data",
113
+ `"${jsonPath}"`,
114
+ "--sheet-type",
115
+ sheetType,
116
+ "--format",
117
+ format
118
+ ];
119
+ await runAsepriteCommand(args);
120
+ generated.push({
121
+ tag: tagName,
122
+ png: pngPath,
123
+ json: jsonPath,
124
+ frames: tag.to - tag.from + 1
125
+ });
126
+ }
127
+ return successResult("character_pipeline_export", {
128
+ inputFile: inputAbs,
129
+ exportDir: exportDirAbs,
130
+ sheetType,
131
+ format,
132
+ generated,
133
+ });
134
+ }
135
+ catch (err) {
136
+ return errorResult("character_pipeline_export", `Character export failed: ${err instanceof Error ? err.message : String(err)}`);
137
+ }
138
+ };
139
+ const character_pipeline_build = async ({ inputFile, tempOutput, exportDir, normalizeOption = { targetMs: 100, autoCrop: true }, exportOption = { sheetType: "packed", format: "json-hash" } }) => {
140
+ try {
141
+ const inputAbs = ensureSafePath(inputFile, { mustExist: true });
142
+ const baseDir = path.dirname(inputAbs);
143
+ const baseName = path.basename(inputAbs, path.extname(inputAbs));
144
+ const normalizedOutput = tempOutput ??
145
+ path.join(baseDir, `${baseName}_normalized.aseprite`);
146
+ const exportDirAbs = ensureSafePath(exportDir, { mustExist: false });
147
+ const analyzeResponse = await character_pipeline_analyze({ inputFile: inputAbs }, {});
148
+ const analyzeContent = analyzeResponse.content[0];
149
+ const analyzeParsed = JSON.parse(analyzeContent.text);
150
+ if (!analyzeParsed.success) {
151
+ return errorResult("character_pipeline_build", analyzeParsed.error);
152
+ }
153
+ const normalizeResponse = await character_pipeline_normalize({
154
+ inputFile: inputAbs,
155
+ saveOutput: normalizedOutput,
156
+ targetMs: normalizeOption.targetMs,
157
+ autoCrop: normalizeOption.autoCrop,
158
+ }, {});
159
+ const normalizeContent = normalizeResponse.content[0];
160
+ const normalizeParsed = JSON.parse(normalizeContent.text);
161
+ if (!normalizeParsed.success) {
162
+ return errorResult("character_pipeline_build", normalizeParsed.error);
163
+ }
164
+ const exportResponse = await character_pipeline_export({
165
+ inputFile: normalizedOutput,
166
+ exportDir: exportDirAbs,
167
+ sheetType: exportOption.sheetType,
168
+ format: exportOption.format,
169
+ }, {});
170
+ const exportContent = exportResponse.content[0];
171
+ const exportParsed = JSON.parse(exportContent.text);
172
+ if (!exportParsed.success) {
173
+ return errorResult("character_pipeline_build", exportParsed.error);
174
+ }
175
+ return successResult("character_pipeline_build", {
176
+ inputFile: inputAbs,
177
+ normalizedFile: normalizedOutput,
178
+ exportDir: exportDirAbs,
179
+ analyze: analyzeParsed.result,
180
+ normalize: normalizeParsed.result,
181
+ export: exportParsed.result,
182
+ });
183
+ }
184
+ catch (err) {
185
+ return errorResult("character_pipeline_build", `Character pipeline failed: ${err instanceof Error ? err.message : String(err)}`);
186
+ }
187
+ };
188
+ return {
189
+ character_pipeline_analyze,
190
+ character_pipeline_normalize,
191
+ character_pipeline_export,
192
+ character_pipeline_build,
193
+ };
194
+ }
195
+ export function createToolSchemas() {
196
+ return {
197
+ character_pipeline_analyze: z.object({
198
+ inputFile: z.string(),
199
+ }),
200
+ character_pipeline_normalize: z.object({
201
+ inputFile: z.string(),
202
+ saveOutput: z.string().optional(),
203
+ targetMs: z.number().optional(),
204
+ autoCrop: z.boolean().optional(),
205
+ }),
206
+ character_pipeline_export: z.object({
207
+ inputFile: z.string(),
208
+ exportDir: z.string(),
209
+ sheetType: z.enum(["packed", "rows"]).optional(),
210
+ format: z.enum(["json-hash", "json-array"]).optional(),
211
+ }),
212
+ character_pipeline_build: z.object({
213
+ inputFile: z.string(),
214
+ tempOutput: z.string().optional(),
215
+ exportDir: z.string(),
216
+ normalizeOption: z.object({
217
+ targetMs: z.number().optional(),
218
+ autoCrop: z.boolean().optional(),
219
+ }).optional(),
220
+ exportOption: z.object({
221
+ sheetType: z.enum(["packed", "rows"]).optional(),
222
+ format: z.enum(["json-hash", "json-array"]).optional(),
223
+ }).optional(),
224
+ }),
225
+ };
226
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iborymagic/aseprite-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for using Aseprite API",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@modelcontextprotocol/sdk": "^1.25.1",
21
- "zod": "^4.2.1"
21
+ "axios": "^1.13.2",
22
+ "zod": "^3.23.8"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/node": "^25.0.0",