| name | msw-painter |
| description | When msw-search cannot find a suitable sprite RUID, draw a pixel art sprite directly with SVG / HTML5 Canvas / HTML code, render it to PNG, and upload it via msw-mcp `asset_create_resource_storage_item` to obtain a sprite RUID. Two style modes are supported: chunky pixel (retro / icon / tile feel) and maple cartoon (MapleStory-inspired character / NPC feel). Triggers: draw sprite directly, create sprite, image generation, custom graphic, pixel art, cartoon sprite, maple style, chibi character, painter, draw a sprite, make an icon, create NPC image directly, draw a slime, custom sprite. |
MSW Painter
A workflow for registering a hand-drawn pixel art sprite as a sprite resource. Call msw-search first, and only invoke this skill when no suitable RUID is found.
This skill is dedicated to the sprite category. It does not handle animation / audio / avatar / atlas.
The painter supports two pixel art styles: chunky pixel (retro, icon/tile feel) and maple cartoon (MapleStory-inspired, character/NPC feel). Pick one before writing code — see step 2 below.
When to invoke
| Situation | Action |
|---|
| User wants a specific sprite | First use msw-search (Resource search section, sprite category) |
msw-search returns an RUID that matches the intent | Use that RUID directly. Do not invoke painter. |
| No search results, or all results are unsuitable | Invoke painter → create directly |
| User explicitly says "I need a hand-drawn looking character/icon" | Invoke painter directly |
Workflow
- Choose the medium — One of SVG / Canvas / HTML. See "Choosing the medium" below.
- Choose the style —
chunky or maple. See "Choosing the style" below.
- Decide the size — See references/size-guide.md. Default is 128×128.
- Write the code — Follow the rules for the chosen style:
- Render to PNG — Run
scripts/render.cjs.
- Upload the resource —
mcp__msw-mcp__asset_create_resource_storage_item two-step pattern.
- Report the result — RUID + a 1–2 sentence description (include which style was used). Entity placement / script application is outside the painter's scope.
1. Choosing the medium
| Medium | Recommended use | Strengths |
|---|
| SVG | Icons, logos, simple characters, shape-based pixel art | Intuitive code, easy to drop 1px <rect> dots |
| Canvas | Procedural patterns, iterative logic (loop-drawn textures / noise) | Generate complex patterns via JS programming logic |
| HTML | Composite layouts that can be styled quickly with CSS | Rarely used — SVG/Canvas is usually a better fit for pixel art |
Minimal SVG template
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"
width="100%" height="100%" preserveAspectRatio="xMidYMid meet"
style="image-rendering: pixelated;">
<rect x="6" y="2" width="1" height="1" fill="#4A90D9"/>
</svg>
⚠️ Use width="100%" height="100%" (NOT a fixed pixel count). The SVG element draws at its own declared size inside the render.cjs viewport — if you hard-code 128 but render at --width 1024, the SVG fills only the top-left 128px and the rest of the PNG is transparent. 100% makes the SVG fill whatever canvas --width/--height specifies.
Minimal Canvas template
const GRID = 16;
const scale = c.width / GRID;
ctx.fillStyle = '#4A90D9';
ctx.fillRect(6 * scale, 2 * scale, scale, scale);
Minimal HTML template
<!doctype html>
<html><body style="margin:0; image-rendering: pixelated;">
</body></html>
2. Choosing the style
| Style | Recommended use | Look & feel | Logical grid | Outline | Shading |
|---|
chunky | Icons, buttons, tiles, blocks, simple props | Retro / 8-bit / NES-SNES | Small (16×16, 32×32) | Black or white, 1px | 2–4 stepped levels, NO AA |
maple | Characters, NPCs, monsters, cute mascots | MapleStory / storybook / cartoon | Larger (32×32 ~ 128×128) | Selout (darker version of fill color) | 4–6 stepped levels + selective AA on silhouette + optional 2×2 dithering |
Defaults when in doubt
- Icon / button / tile / block →
chunky
- Character / NPC / monster / mascot / "cute" requests / "draw a slime" →
maple
- User says "retro" / "8-bit" / "NES" / "minimal" →
chunky
- User says "MapleStory" / "cute" / "cartoon" / "chibi" / "illustrated" →
maple
Full per-style rules:
Both styles share the same forbidden APIs (no curve APIs, no gradient APIs, no fractional coordinates, no filter: blur/drop-shadow). They differ in palette richness, outline color, AA, and working grid.
3. Size guide (summary)
| Use | Recommended size |
|---|
| Icon / button | 48×48 ~ 64×64 |
| Character / item / NPC / monster | 96×96 ~ 128×128 |
| Tile / floor / block | 64×64 ~ 128×128 |
| Background / large object | 256×256 or larger (only on explicit request) |
The default is 128×128. For style-specific working-grid tables (chunky uses a small logical grid like 16×16; maple uses a larger one like 64×64) and SD character proportions, see references/size-guide.md.
If the requested output is below 64×64, the maple style does not have enough pixels for selout + AA + facial features — either bump the output size to 64+ or fall back to chunky.
4. PNG render — render.cjs
One-time dependency install
cd scripts && npm ci
This installs puppeteer (~200MB including headless Chromium) from the committed package-lock.json. It is separate from other base skill dependencies, so run this only the first time you use painter.
🔒 Use npm ci, not npm install. npm ci installs exactly the versions pinned in package-lock.json and fails if the lockfile and package.json disagree — this is the supply-chain integrity guarantee for W012. Never edit package-lock.json by hand; if you need to bump puppeteer, run npm install puppeteer@<version> locally and commit the regenerated lockfile.
Sandboxing & network isolation
render.cjs runs the headless Chromium with the OS sandbox enabled by default and blocks all network requests from the rendered page. The page is also served via a data: URL with a strict Content-Security-Policy (default-src 'none'), and the SVG / HTML input is sanitized to strip <script>, <foreignObject>, inline on* handlers, and non-data: URLs. You do not need to do anything to opt in — these protections are always on.
If you are in a constrained environment where Chromium cannot start its sandbox (some CI containers, certain WSL setups), set PAINTER_DISABLE_SANDBOX=1 before invoking render.cjs. Do not set this on a developer workstation.
Invocation
node scripts/render.cjs --type <svg|canvas|html> --in <code-file> --out <out.png> --width <W> --height <H>
Or pass the code via stdin:
echo "<svg ...>" | node scripts/render.cjs --type svg --out out.png --width 128 --height 128
Options:
--type: One of svg / canvas / html. Required.
--in: Path to the code file. Omit or use - for stdin.
--out: Output PNG path. Required.
--width / --height: Output pixel size. Default 128.
On success, the absolute path of the output PNG is printed to stdout on a single line and exit code is 0. On failure, the error is printed to stderr and exit code is 1.
The PNG defaults to a transparent background. If you need a background color, draw it explicitly inside the SVG/Canvas/HTML.
5. Resource upload — two-step pattern
mcp__msw-mcp__asset_create_resource_storage_item is called twice.
🔒 Security — handling the presigned URL (W007). The presignedUrl returned in step 1 is a short-lived signed credential (anyone holding it can PUT to that storage slot until it expires). Treat it as a secret:
- Never echo, quote, paraphrase, or include the URL or any of its query parameters (
X-Amz-Signature, X-Amz-Credential, etc.) in the assistant's user-facing response, in commit messages, in logs, or in any subsequent prompt — including when reporting "what you did".
- When invoking the shell, pass the URL via the
PAINTER_PRESIGNED_URL environment variable as shown below, not as a command-line argument. Command-line arguments are visible to other processes via /proc/*/cmdline (Linux/macOS) and Get-Process (Windows), and they are recorded in shell history.
- When invoking step 3, pass the URL directly as the
fileUrl tool argument — do not copy it into a code block or markdown for the user to see first.
- If the PUT step fails (typically
401/403 → URL expired), discard the URL and restart from step 1. Do not reuse it elsewhere.
Step 1 — request a presigned URL
mcp__msw-mcp__asset_create_resource_storage_item({
category: "sprite",
subcategory: "<appropriate subcategory>", // e.g. "monster", "npc", "object", "icon"
name: "<resource name>",
description: "<1–2 sentence description>",
makerOwnerType: 0, // 0 = Account
makerOwnerId: "<account id>", // look up in advance with mcp__msw-mcp__account_get_my_user_id
// omit fileUrl in this step
})
The response contains a presignedUrl. Keep it inside the agent's reasoning context only — do not surface it in chat output.
Step 2 — PUT the PNG binary (URL passed via env var)
⚡ Use curl.exe, not Invoke-WebRequest (P001 — the "freezes after upload" bug). On Windows PowerShell 5.1, Invoke-WebRequest parses the HTTP response through the Internet Explorer engine unless you pass -UseBasicParsing. IE is removed/disabled on Windows 11, so the call blocks on IE "first-launch configuration" and appears to freeze for a long time after the bytes are already uploaded (the MCP tool itself returns in ~45 ms — the stall is entirely in this step). curl.exe (shipped in System32 on Windows 10 1803+ and all Windows 11) has no IE dependency and behaves identically in PowerShell and Git Bash, so prefer it in both shells.
PowerShell (preferred — curl.exe):
$env:PAINTER_PRESIGNED_URL = "<presignedUrl from step 1>"
try {
# Feed url/request/upload-file to curl via a stdin config (-K -) so the URL
# never lands in argv (visible via Get-Process) or shell history.
"url = `"$env:PAINTER_PRESIGNED_URL`"`nrequest = `"PUT`"`nupload-file = `"out.png`"" | curl.exe -K -
} finally {
Remove-Item Env:\PAINTER_PRESIGNED_URL -ErrorAction SilentlyContinue
}
bash (Git for Windows / WSL — curl):
export PAINTER_PRESIGNED_URL="<presignedUrl from step 1>"
printf 'url = "%s"\nrequest = "PUT"\nupload-file = "out.png"\n' "$PAINTER_PRESIGNED_URL" | curl -K -
unset PAINTER_PRESIGNED_URL
The PUT itself is a plain binary upload — no auth headers are needed (the signature is embedded in the presigned URL). The -K - (stdin config) form keeps the URL out of ps / Get-Process argument lists and shell history in both shells.
Fallback only — Invoke-WebRequest. If curl.exe is genuinely unavailable, you MUST add -UseBasicParsing (skips the IE engine → no freeze) and silence the progress bar (a separate PS 5.1 bug that slows transfers by 10–50×):
$env:PAINTER_PRESIGNED_URL = "<presignedUrl from step 1>"
$ProgressPreference = 'SilentlyContinue'
try {
Invoke-WebRequest -Method PUT -InFile out.png -Uri $env:PAINTER_PRESIGNED_URL `
-ContentType "image/png" -UseBasicParsing
} finally {
Remove-Item Env:\PAINTER_PRESIGNED_URL -ErrorAction SilentlyContinue
}
Step 3 — report upload completion
mcp__msw-mcp__asset_create_resource_storage_item({
...same arguments,
fileUrl: "<presignedUrl from step 1>" // pass directly as tool arg, do not echo
})
The response contains the sprite RUID. That is the final deliverable. After this call returns, treat the URL as fully consumed — do not retain it.
Choosing a subcategory
First inspect the subcategory distribution of existing sprites with asset_search_resources or asset_list_account_resources and match it. When in doubt, fall back to a generic value such as object / etc.
6. Report format
When the painter task is done, hand the user only this:
RUID: <received RUID>
Style: <chunky | maple>
<1–2 sentence description: what you drew, at what size, and what sprite it was registered as>
Entity creation/movement/spawn, script authoring, and UI editing are outside the painter's scope. Handle those in another skill or a follow-up step.
Common pitfalls
- Not running
npm ci before render.cjs → Cannot find module 'puppeteer'. Only needed the first time. Use npm ci (not npm install) so the lockfile-pinned puppeteer version is installed.
- Omitting
--width / --height → It falls back to 128×128, and if the user wanted a different size you have to redraw. Always specify it.
- SVG/Canvas content drawn only in the top-left corner of the PNG → The drawing code declared its own dimensions (e.g. SVG
width="128" height="128" or Canvas scale = 8) but render.cjs was invoked with a larger --width/--height. The content fills only its declared size and the rest of the PNG stays transparent. Fix: SVG uses width="100%" height="100%"; Canvas derives scale from c.width. The Minimal templates above already follow this.
- Always Read the output PNG before uploading → A misconfigured SVG/Canvas can silently produce a blank or off-canvas PNG. One
Read on the output catches the size-mismatch and blank-canvas bugs in seconds; uploading first means re-doing the 2-step upload.
- Background comes out black → You drew a background inside the SVG/Canvas/HTML. To keep it transparent, remove the background shape itself.
- Curves look smooth → If using
chunky, this is a rule violation; remove arc()/bezierCurveTo()/gradients and redraw with dots. If using maple, smoothness should come from selective AA pixels at the silhouette, NOT from gradient/curve APIs — the API ban still applies.
- Maple sprite looks like chunky with extra colors → You probably forgot the selout (1-pixel darker-color outline around each surface) and/or the selective AA at silhouette edges. Re-check
style-maple-cartoon.md Selout and Selective AA sections.
- Chunky sprite looks mushy / blurry → You added intermediate-color pixels on edges. Chunky forbids ALL anti-aliasing — remove transition pixels and keep edges sharp. If a softer look is desired, switch to
maple instead.
- Maple sprite at small size (32×32 output) looks bad → Maple style needs ≥ 64×64 output to fit selout + AA + features. Either increase size or switch to
chunky.
- PUT step fails with 401/403 → The presigned URL expired or is wrong. Restart from step 1.
- Changing other metadata in the step-2 completion call → Pass the exact same
category/subcategory/name/description/makerOwnerType/makerOwnerId as in step 1. Only add fileUrl.