@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 +20 -19
- package/build/aseprite/cli.js +11 -3
- package/build/aseprite/tools.js +0 -6
- package/build/generator/image-generator.js +55 -0
- package/build/generator/tools.js +210 -0
- package/build/index.js +38 -16
- package/build/lua/cli.js +16 -0
- package/build/lua/template.js +79 -0
- package/build/lua/tools.js +32 -15
- package/build/pipeline/character.js +66 -0
- package/build/pipeline/tools.js +226 -0
- package/package.json +3 -2
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
|
-
- `
|
|
22
|
-
- `
|
|
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
|
|
44
|
-
Add the following to your
|
|
55
|
+
### Using with Claude Desktop
|
|
56
|
+
Add the following to your claude_desktop_config.json
|
|
45
57
|
```json
|
|
46
58
|
{
|
|
47
|
-
"
|
|
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 .
|
|
69
|
+
Add the following to your mcp.json
|
|
69
70
|
```json
|
|
70
71
|
{
|
|
71
72
|
"mcpServers": {
|
package/build/aseprite/cli.js
CHANGED
|
@@ -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
|
}
|
package/build/aseprite/tools.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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);
|
package/build/lua/cli.js
ADDED
|
@@ -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
|
+
}
|
package/build/lua/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { errorResult, successResult } from "../util.js";
|
|
2
|
-
import { findLuaTemplate, LUA_TEMPLATES
|
|
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
|
|
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:
|
|
35
|
-
stderr:
|
|
46
|
+
stdout: stdoutTrimmed,
|
|
47
|
+
stderr: stderrTrimmed
|
|
36
48
|
});
|
|
37
49
|
}
|
|
38
50
|
catch (err) {
|
|
39
|
-
return errorResult("aseprite_run_lua_template",
|
|
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
|
|
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:
|
|
57
|
-
stderr:
|
|
79
|
+
stdout: stdoutTrimmed,
|
|
80
|
+
stderr: stderrTrimmed
|
|
58
81
|
});
|
|
59
82
|
}
|
|
60
83
|
catch (err) {
|
|
61
|
-
return errorResult("aseprite_run_lua_script",
|
|
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.
|
|
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
|
-
"
|
|
21
|
+
"axios": "^1.13.2",
|
|
22
|
+
"zod": "^3.23.8"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^25.0.0",
|