@iborymagic/aseprite-mcp 0.2.0 → 0.3.1

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,7 +2,6 @@
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.
7
6
 
8
7
  ## Features Overview
@@ -18,9 +17,8 @@ Adds deeper control using Aseprite Lua scripting, enabling safe AI-driven editin
18
17
  - `aseprite_list_lua_templates`: Lists available Lua templates
19
18
  - `aseprite_run_lua_template`: Runs a predefined safe Lua automation(templates)
20
19
  - `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
20
+ - `recolor_palette`: Palette recoloring
21
+ - `normalize_animation_speed`: Normalizing animation speed
24
22
  - `auto_crop_transparent`: Removing empty transparent borders around the sprite
25
23
  - `export_layer_only`: Exporting only a specific layer as a flattened PNG image
26
24
  - `export_tag_frames`: Exporting all frames within a specific animation tag as individual PNG files
@@ -40,11 +38,11 @@ npm run build
40
38
  npx aseprite-mcp
41
39
  ```
42
40
 
43
- ### Using with ChatGPT
44
- Add the following to your mcp.json
41
+ ### Using with Claude Desktop
42
+ Add the following to your claude_desktop_config.json
45
43
  ```json
46
44
  {
47
- "servers": {
45
+ "mcpServers": {
48
46
  "aseprite-mcp": {
49
47
  "command": "npx",
50
48
  "args": ["-y", "aseprite-mcp"]
@@ -53,19 +51,8 @@ Add the following to your mcp.json
53
51
  }
54
52
  ```
55
53
 
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
54
  ### Using with Cursor
68
- Add the following to your .cursor.json
55
+ Add the following to your mcp.json
69
56
  ```json
70
57
  {
71
58
  "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.1",
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",