
Msw Search
- 4.3k installs
- 33 repo stars
- Updated July 29, 2026
- msw-git/msw-ai-coding-plugins-official
msw-search is an agent skill that routes MapleStory Worlds API documentation vector search and resource RUID lookup through msw-mcp retrievers and the MSW resource REST API.
About
msw-search routes MapleStory Worlds lookup work across two distinct targets: vector search for API documentation and implementation guides via the msw-mcp MCP server, and REST resource search for sprites, animations, sounds, resource packs, and avatars. A routing table steers implementation questions to mlua_api_retriever or mlua_document_retriever while RUID needs default to resource_pack first so agents pick complete asset bundles instead of stray single-frame sprites. The skill mandates reading .d.mlua type stubs before vector search, documents Maker Editor to .mlua conversion rules including LEA-3014 ExecSpace override pitfalls, and requires all resource API calls through scripts/msw_resource_api.cjs for UTF-8 safe JSON. Developers use it when .d.mlua signatures are insufficient, when they need code examples or related APIs, when semantic search is required without knowing an API name, or when obtaining SpriteRUID values for monsters, backgrounds, BGM, or avatar items without fabricating identifiers.
- Routes document vector search and REST resource search through explicit decision tables.
- Defaults resource_pack search before individual sprite or animationclip RUID lookups.
- Documents .d.mlua first research order plus mlua_api_retriever and mlua_document_retriever tools.
- Covers Maker Editor to .mlua conversion and LEA-3014 ExecSpace override warnings.
- Requires msw_resource_api.cjs wrapper instead of hand-built curl for resource queries.
Msw Search by the numbers
- 4,319 all-time installs (skills.sh)
- +542 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #176 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
msw-search capabilities & compatibility
- Capabilities
- vector api and guide search via msw mcp retrieve · rest resource search for sprites animations soun · maker editor to .mlua syntax conversion guidance · .d.mlua signature first api research ordering
- Works with
- openai
- Use cases
- research · api development
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.3k |
|---|---|
| repo stars | ★ 33 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 29, 2026 |
| Repository | msw-git/msw-ai-coding-plugins-official ↗ |
How do I find MSW API examples, implementation guides, or valid sprite and animation RUIDs when .d.mlua stubs lack enough detail?
Search MSW API docs via msw-mcp vector retrievers and locate sprite, animation, sound, and avatar RUIDs through the resource REST API.
Who is it for?
Developers scripting MapleStory Worlds who need API examples, semantic doc search, or asset RUID discovery via MCP and REST.
Skip if: Skip when you are not building MapleStory Worlds content or only need generic JavaScript API docs.
When should I use this skill?
User asks for MSW API examples, implementation guides, SpriteRUID lookup, monster sprites, sounds, or resource pack search.
What you get
Targeted API documentation hits, implementation patterns, and verified resource RUIDs obtained without guessing identifiers.
- API documentation matches
- sprite and sound RUID references
By the numbers
- Two search targets: document vector search and REST resource search
- Default resource search type is resource_pack
Files
MSW Search
MSW has two distinct search targets:
1. API docs & implementation guides — Vector search for descriptions, code examples, and related APIs missing from .d.mlua. 2. Resources — REST API for sprites, animations, sounds, resource packs, and avatars. The only path for obtaining RUIDs.
---
Routing Table
| Request type | Go to section |
|---|---|
| "How do I implement this?", "Show me an example", "What related APIs exist?" | Document search |
| ".d.mlua only has the signature; the description is insufficient" | Document search |
| "I don't know the API name (semantic search)" | Document search |
| "Implementation guide / best practice / pattern" | Document search |
| "I need a SpriteRUID", "Find a sprite for monster / NPC / background" | Resource search → start with `resource_pack` |
| "Find an animation / sound / resource pack" | Resource search → start with `resource_pack` |
| "Details for this RUID", "Similar resources" | Resource search |
| "Avatar item / default avatar lookup" | Resource search |
| "Upload / list / update / delete my own assets" | Call msw-mcp asset_* tools directly (account_get_my_user_id first for ownerId) |
★ Resource search default — always `resource_pack` first
>
Unless the user explicitly asks for an individual sprite / animationclip / sound / avatar item (or names a non-pack RUID directly), passresourceTypeFilter: ["resource_pack"]tosearchResources. A pack bundles every sprite + animation + sound for one asset, so picking a strayspriteoranimationclipfirst usually leaves the entity with a single frame, no animation set, or the wrong asset family.
>
Search the pack → drill into payload.elements → assign individual RUIDs.Switch types only on explicit intent: "BGM file", "individual sprite only", "avatar item", "animationclip similar to this RUID", etc.
---
Section 1 — Document Search (APIs & Guides)
Vector search via the `msw-mcp` MCP server. Supplies the detailed descriptions, code examples, related APIs, and implementation guides missing from .d.mlua.
Decision Flow
Need API-related information
│
├─ Checking signature / type / property / enum
│ → Read .d.mlua first (highest priority)
│ → If .d.mlua is insufficient, call msw-mcp
│ (code examples, parameter details, related APIs, etc.)
│
├─ Implementation guide / pattern / best practice
│ → mlua_document_retriever
│
└─ Don't know the API name (semantic search)
→ mlua_api_retriever (and/or mlua_document_retriever for broader scope)---
API Research Order
Priority 1 — .d.mlua (always first)
If you know the API name, always read `.d.mlua` first. Signatures, types, properties, event parameters, and enum values can be confirmed here accurately.
Path: Environment/NativeScripts/{Component,Service,Event,Enum,Logic,Misc}/Name.d.mlua
| Situation | Example |
|---|---|
| Confirm method signature | "Does TransformComponent have SetPosition?" |
| Property type / existence | "What is the type of SpriteRendererComponent.RUID?" |
| Event parameter structure | "What are the AttackEvent constructor parameters?" |
| List of enum values | "What are the BodyMoveType values?" |
| Method existence | "What methods does SpawnService have?" |
Priority 2 — Vector search (when .d.mlua is not enough)
.d.mlua contains only signatures and lacks detailed descriptions and examples. Use vector search when you need any of the following.
| Situation | MCP tool | Example query |
|---|---|---|
| Need a code example | mlua_api_retriever | AIComponent example, BehaviorTree usage |
| Parameter details | mlua_api_retriever | BadgeService GetBadgeInfosAndWait parameters |
| Related API cross-references | mlua_api_retriever | AttackComponent related, HitComponent |
| ScriptOverridable check | mlua_api_retriever | AttackComponent CalcCritical override |
| Don't know the API name | both retrievers | damage calculation, inventory save |
| "How do I …?" implementation guide | mlua_document_retriever | how to make inventory system |
| Pattern / best practice | mlua_document_retriever | collision detection best practice |
---
MCP Tools (msw-mcp)
| Tool | Description |
|---|---|
| `mlua_api_retriever` | API details for Service / Component / Misc etc. (signatures, parameters, examples). Pass an API/class/function/component name. |
| `mlua_document_retriever` | Authoring manuals, guidelines, MLua usage, and other document-style material. Pass a natural-language sentence describing what to implement. |
On failure: If a msw-mcp tool call errors out, surface the failure to the user and fall back to .d.mlua. Do not guess — state what you couldn't verify.
Default result count: request 3 results unless wider exploration is explicitly required.
---
.d.mlua vs Search — Information Comparison
.d.mlua is a type stub (~29 lines); Search returns the full document (254+ lines).
| Information | .d.mlua | Search |
|---|---|---|
| Method signature / types | O | O |
| Property declarations | O | O |
| Detailed method description (DetailDesc) | X | O |
| Code examples (AdditionalPageContent) | X | O |
| Per-parameter descriptions | X | O |
| Related APIs (SeeAlsoAPIs) | X | O |
| Related guides (SeeAlsoGuides) | X | O |
| ScriptOverridable flag | X | O |
| SyncDirection | Partial | O |
| Localized descriptions (Ko/Ja/Es/Zh) | X | O |
---
Maker Editor Syntax → .mlua Conversion Rules
Code examples in search results use Maker Editor syntax. They must be converted before being used in a local .mlua file.
| Item | Maker Editor | .mlua file | Note |
|---|---|---|---|
| Override declaration | override integer CalcDamage(...) | method integer CalcDamage(...) | override → method |
| Block | { ... } | ... end | Braces → end |
| Exec space (own method) | [server only] | @ExecSpace("ServerOnly") | Self-defined methods: annotate explicitly |
| Exec space (override) | [server only] shown / omitted in editor | Match the parent's `@ExecSpace` exactly — see warning below | LEA-3014 if mismatched |
| Property | Property: int32 Score = 0 | @Sync property int32 Score = 0 | Add @Sync if synced |
Type int | int | integer | C# int → mlua integer |
Type number | number | number | Same (double) |
Type float | float | float | Same (single) |
number(64-bit double) andfloat(32-bit single) are assignable to each other but remain distinct types. Follow the.d.mluadeclaration.
⚠ Override ExecSpace caveat — LEA-3014 `SignatureMismatch`
>
The Maker Editor often hides the parent's exec space and lets you toggle[server only]freely on anoverrideblock. In.mlua, however, the override's@ExecSpacemust be byte-identical to the parent declared in.d.mlua. If the parent has no@ExecSpace(engine default =ExecSpace=All), the override must also omit@ExecSpaceentirely.
>
Concretely, the AttackComponent / HitComponent damage hooks (CalcDamage,CalcCritical,GetCriticalDamageRate,GetDisplayHitCount,IsAttackTarget,IsHitTarget,OnAttack) are allExecSpace=Allupstream. Adding@ExecSpace("ServerOnly")produces:
>
```
[LEA-3014] SignatureMismatch : The signature of <Child>.CalcDamage[... (ExecSpace=ServerOnly)]
must match the overridden <Parent>.CalcDamage.[... (ExecSpace=All)].
```
>
Always look up the parent in .d.mlua first and copy its annotation block verbatim. Detail: `msw-scripting/SKILL.md` §9 "Method override → LEA-3014".Conversion example — AttackComponent from search results:
-- Maker Editor syntax (search result)
override int CalcDamage(Entity attacker, Entity defender, string attackInfo) {
return 50
}
override boolean CalcCritical(Entity attacker, Entity defender, string attackInfo) {
return _UtilLogic:RandomDouble() < 0.3
}-- Converted to .mlua
-- ⚠ Parent AttackComponent.CalcDamage / CalcCritical declare no @ExecSpace
-- (ExecSpace=All). Adding @ExecSpace here triggers LEA-3014 SignatureMismatch.
method integer CalcDamage(Entity attacker, Entity defender, string attackInfo)
return 50
end
method boolean CalcCritical(Entity attacker, Entity defender, string attackInfo)
return _UtilLogic:RandomDouble() < 0.3
end---
Section 2 — Resource Search (Sprite / Animation / Sound / Resource Pack / Avatar)
REST API for searching and browsing MSW resources. Never guess or fabricate a RUID — always obtain one through this API.
Default search type = `resource_pack` — see the pack-first rule under the Routing Table above.
Access — always go through msw_resource_api.cjs
All resource-API calls in this skill are made through the Node.js wrapper
scripts/msw_resource_api.cjsDo not assemble curl commands by hand. The wrapper:
- Sends UTF-8 JSON bodies directly, so non-ASCII queries (Korean / Japanese / Chinese / emoji) avoid the
{"detail":"There was an error parsing the body"}failure mode that hits inlinecurl -d '...'. - URL-encodes slash-containing path parameters (e.g. pack IDs like
npc/1013617.img). - Zero dependencies (Node 18+ built-in
fetch/AbortController). - Uses the exact OpenAPI field names (
topK,resourceTypeFilter,categoryFilter,count, …). Legacy names likelimit/types/categoriesare silently ignored by the server.
Two ways to use it:
# 1) CLI — fire one call from a shell. Output is pretty-printed JSON.
node scripts/msw_resource_api.cjs \
search "orange mushroom" --resource-type resource_pack --category npc --topK 3
# Discover available subcommands:
node scripts/msw_resource_api.cjs --help// 2) require — preferred when already in a Node.js context.
const {
searchResources, searchAvatarItems, findSimilarResources,
getResource, getResourcesBatch, getResourceTags,
listResources, randomResources, findPacksContaining,
listAvatars, getAvatarDefaults,
} = require('./scripts/msw_resource_api.cjs');
const result = await searchResources("orange mushroom", {
resourceTypeFilter: ["resource_pack"],
categoryFilter: ["npc"],
topK: 3,
});Wrapper function ↔ endpoint map
| Wrapper function | CLI subcommand | Endpoint |
|---|---|---|
searchResources | search | POST /v3/search/resources |
searchAvatarItems | search-avatar | POST /v3/search/resources (avatar mode) |
findSimilarResources | similar | GET /v3/search/resources/similar/{ruid} |
getResource | get | GET /v3/resources/{ruid} (works for sprite / animationclip / resource_pack / avataritem) |
getResourcesBatch | batch | POST /v3/resources/batch |
getResourceTags | tags | GET /v3/resources/tags/{ruid} |
listResources | list | GET /v3/resources (Qdrant Scroll, opaque-string offset cursor) |
randomResources | random | GET /v3/resources/random |
findPacksContaining | packs | GET /v3/resources/packs/{ruid} (lists packs containing a RUID — pack id is NOT accepted here) |
listAvatars | avatars | GET /v3/avatars |
getAvatarDefaults | avatar-defaults | GET /v3/avatars/defaults |
No `/v3/avatars/{ruid}` endpoint exists. To inspect an avataritem
(color_hex, group members, …), call getResource(ruid) — the/v3/resources/{ruid} endpoint returns avataritem detail just likeany other resource.
Base URL & transport (informational)
The wrapper handles all of this — you do not need to set it manually.
- Base URL:
https://maplestoryworlds-resourcesearch-new.nexon.com/api - No auth (public),
/v3/prefix, POST bodies areapplication/json; charset=utf-8 - Default timeout: 15s (override via the wrapper's
_request(method, path, { timeout }))
Result count — this skill's default is 3
Unless explicitly told otherwise, always send `3` for the result-count parameter on every search call. The wrapper defaults to 3 as well, and parameter names follow the OpenAPI spec exactly — note that limit / count / topK differ per endpoint.
| Endpoint | Server parameter | Wrapper default |
|---|---|---|
POST /v3/search/resources (resources + avatar) | topK | 3 |
GET /v3/search/resources/similar/{ruid} | topK | 3 |
GET /v3/resources (browsing) | limit | 3 |
GET /v3/resources/random | count | 3 |
GET /v3/resources/packs/{ruid} (packs containing a RUID) | limit | 3 |
The server-side default is 20 or 50, so always pass these parameters explicitly.
Increase to 10+ (or 50–100 for avatar broad-browse) only when wider exploration is explicitly required.
`offset` parameter caveat — forGET /v3/resourcesandGET /v3/resources/packs/{ruid},
offset is not an integer but the opaque string cursor `nextOffset` returned by the previous response.Do not send it on the first page (sending integer 0 is interpreted as a cursor and returns empty results).POST body rule — let msw_resource_api.cjs handle it
If you must POST without the wrapper (no HTTP client in your language), reproduce its behaviour:
1. Serialize the body as UTF-8 JSON bytes (not a re-encoded shell string). 2. Send Content-Type: application/json; charset=utf-8. 3. POST raw bytes (e.g. curl's --data-binary "@file" reading a UTF-8 temp file).
Otherwise, just call the wrapper.
Resource Types
type values (the type field on server responses, and the values you put into the resourceTypeFilter array when searching):
| type | Description |
|---|---|
sprite | Static image (PNG) |
animationclip | Frame-based animation |
resource_pack | Finished asset bundling sprites + animations + sounds |
bgm | Background music (audio) |
voice | Voice clip — NPC dialogue, etc. (audio) |
effect | Sound effect (audio). Not a visual effect. For visual particles / hit / skill FX, search sprite or animationclip (categories skill / mob / etc). |
avataritem | Avatar costume item (cap, coat, pants, shoes, weapon, …) — same POST /v3/search/resources endpoint with resourceTypeFilter: ["avataritem"]. See `references/resource/search.md` ("Avatar Item Search") and `references/resource/avatar.md`. |
All search and listing endpoints use the same type-filter field name: `resourceTypeFilter`
(an array). Other names like types are silently ignored by the server.The wrapper'sresource_type_filterargument (or CLI--resource-type) maps to this field.
⚠ `SpriteRendererComponent.SpriteRUID` accepts both `sprite` and `animationclip`, but renders them differently:
- animationclip → all frame layers play (shadow + body + foreground)- sprite → that single Sprite renders only>
Symptom of mistake: feeding ananimationclipRUID where you intended asprite(or vice-versa) leaves only the shadow layer visible — the body silently vanishes. Always checkpayload.typeof the response before assigning toSpriteRUID. Usespritefor the static idle/default frame; useanimationcliponly for fields likeStateAnimationComponent.ActionSheetvalues.
>
`skeleton` and `avataritem` RUIDs fail silently (no error, nothing renders) when assigned to `SpriteRUID` / `ImageRUID` without the `thumbnail://` prefix. Conversely,CostumeManagerComponent.Custom*Equip/SkeletonRendererComponent.SkeletonRUID/StateAnimationComponent.ActionSheetdo not accept thethumbnail://prefix — pass a plain RUID there. If the search query targeted an icon / thumbnail image and returned aspriteRUID, that RUID is already renderable directly — addingthumbnail://is redundant. Full assignment rules — accepted types, slot-by-slot prefix matrix, RUID-vs-prefix usage — live in `msw-sprite-ruid/SKILL.md`.
Categories
category values that actually appear on responses. Use these with categoryFilter.
General resources (sprite / animationclip / resource_pack / bgm / voice / effect)
| category | Description |
|---|---|
mob | Monster |
npc | NPC |
item | Item |
skill | Skill effect / skill resources |
object | Map object (tree, rock, decoration) |
background | Background / map tile / BGM |
foothold | Walkable platform |
rope | Rope |
ladder | Ladder |
etc | Uncategorized |
Avatar (avataritem only)
| category | Slot |
|---|---|
cap, hair, face, faceaccessory, eyeaccessory, earaccessory | Head / face |
coat, longcoat, pants, shoes, glove, cape | Body |
weapon, twohandweapon, subweapon, shield | Weapon |
map,effect,uiare not valid category values — they return zero results.
- Looking for maps / backgrounds →category: "background"or"object".
- Looking for visual effects → searchsprite/animationclipwithcategory: "skill"(ormob/etc);effectis the audio resource_type, not a category.
- There is nouiresource family in this index — UI sprites usually live assprite+category: "etc".
RUID
A 32-character hex string that uniquely identifies every resource. Example: "0017da7385e04bc4b2ddbe5949b4b462"
- The
idfield in search results is the RUID assetGuidis a separate Unity asset GUID (used inspawn_preset)- Never guess or fabricate a RUID — always obtain it from an API response
Common Response Fields
{
"id": "32-char hex RUID",
"type": "sprite|animationclip|resource_pack|bgm|voice|effect|avataritem",
"category": "mob|npc|item|skill|object|background|foothold|rope|ladder|etc | <avatar slot>",
"names": {
"ko": ["Korean name"],
"en": ["English name"]
},
"assetGuid": "Unity asset GUID (may or may not exist)",
"payload": {
"width": 64,
"height": 64,
"thumbnail": "https://...",
"pivot": {"x": 32, "y": 32},
"frames": [],
"elements": []
}
}Pagination — same name, two flavors
nextOffset appears in every list-style response but means different things depending on the endpoint. Round-tripping a value into the wrong endpoint silently misbehaves.
| Endpoint | nextOffset type | Meaning | How to paginate |
|---|---|---|---|
POST /v3/search/resources (search) | integer | Item offset (0-based) | Pass it back as offset (number) |
GET /v3/search/resources/similar/{id} (similar) | integer | Item offset | Same |
GET /v3/resources (list) | opaque UUID string | Qdrant Scroll cursor | Pass the string back as offset. End-of-stream = `null` |
GET /v3/resources/packs/{ruid} (packs) | opaque UUID string | Same cursor | Same |
GET /v3/resources/random | n/a | No pagination | — |
Rules:
1. Never feed a list cursor into a search call (or vice versa) — the server ignores the wrong-shape value and returns the first page. 2. On the first page, omit offset entirely. Sending integer 0 to list / packs is interpreted as a cursor and yields zero items (silent failure). 3. Stop paginating when the response returns nextOffset: null (list / packs) or returns fewer items than topK (search / similar).
Endpoint Summary
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /v3/search/resources | Natural-language semantic search (incl. avatar items via resourceTypeFilter: ["avataritem"]) |
| GET | /v3/search/resources/similar/{ruid} | Find similar resources |
| GET | /v3/resources/{ruid} | Single resource details (sprite / animationclip / resource_pack with populated elements / avataritem) |
| POST | /v3/resources/batch | Batch fetch multiple resources |
| GET | /v3/resources/tags/{ruid} | AI-generated multilingual tags |
| GET | /v3/resources | List resources (Qdrant Scroll, opaque-string offset cursor) |
| GET | /v3/resources/random | Random resource recommendation |
| GET | /v3/resources/packs/{ruid} | List resource packs containing the given RUID — the path parameter is a 32-char-hex RUID, not a pack id |
| GET | /v3/avatars | List all avatar items (cached) |
| GET | /v3/avatars/defaults | Default avatar body / head RUIDs |
Single avataritem detail uses /v3/resources/{ruid} (no/v3/avatars/{ruid} endpoint exists).---
Resource Routing Guide
★ When in doubt, search `resource_pack` first. Only the rows marked with an
explicit non-pack intent below should bypass the pack-first default.
| Situation | Wrapper call (CLI subcommand) | Reference file |
|---|---|---|
| "Find a slime / orange mushroom / monster / NPC / item / background / map asset" (default — no type specified) | searchResources(query, { resourceTypeFilter: ["resource_pack"], ... }) (search ... --resource-type resource_pack) | `references/resource/search.md` |
| "Find an individual sprite / single image" (user explicitly asked for a sprite) | searchResources(query, { resourceTypeFilter: ["sprite"], ... }) | `references/resource/search.md` |
| "Find an individual animationclip" (user explicitly asked for an animation) | searchResources(query, { resourceTypeFilter: ["animationclip"], ... }) | `references/resource/search.md` |
| "Find a visual effect / particle / hit FX" | searchResources(query, { resourceTypeFilter: ["animationclip","sprite"], categoryFilter: ["skill","mob","etc"] }) — note: effect here would mean audio, not visual | `references/resource/search.md` |
| "Find a sound / BGM / voice / sound-effect" (audio) | `searchResources(query, { resourceTypeFilter: ["bgm"\ | "voice"\ |
| "Find a background / map tile / scenery" | searchResources(query, { resourceTypeFilter: ["sprite","animationclip"], categoryFilter: ["background","object"] }) — there is no map category in the index | `references/resource/search.md` |
| "Find a costume / hat / shoes / weapon (avatar item)" | searchAvatarItems(...) (search-avatar) | `references/resource/search.md` (Avatar Item Search section) + `references/resource/avatar.md` |
| "Any more monsters like this one?" | findSimilarResources(ruid, ...) (similar) | `references/resource/search.md` |
| "Details for RUID abc123" (any type incl. avataritem and resource_pack) | getResource(ruid) (get) | `references/resource/detail.md` |
| "Show me a list of monster sprites" | listResources(...) (list) | `references/resource/browse.md` |
| "Which resource packs include this RUID?" | findPacksContaining(ruid, ...) (packs) | `references/resource/browse.md` |
| "Browse all avatar items" | listAvatars(...) (avatars) | `references/resource/avatar.md` |
Typical Workflow (pack-first)
1. searchResources(query, { resourceTypeFilter: ["resource_pack"], topK: 3 })
→ obtain a resource_pack RUID (or pack id like "npc/9072309.img")
→ switch types only on explicit user intent, or fall back when 0 packs match
2. getResource(id)
→ resource_pack: payload.elements is pre-populated with element payloads
(sprite / animationclip / sound RUIDs live here)
→ avataritem: payload has color_hex / group meta
3. Pick the element from payload.elements and assign its RUID to
SpriteRendererComponent.SpriteRUID / StateAnimationComponent.ActionSheet
(or assign avataritem RUIDs through the slot mapping in `msw-avatar`)Don't callfindPacksContaining(packId)to "open" a pack — that endpoint takes a 32-hex RUID and returns the packs that include that RUID, not the contents of a pack. UsegetResource(packId)for pack contents.
For detailed Request/Response of each endpoint, refer to the files under `references/resource/`.
---
Sprite Orientation — Most Resources Face Left
Most MSW sprite / animationclip / resource_pack assets — especially mob, npc, and player-character — are authored facing left, so a freshly spawned SpriteRendererComponent renders left unless you flip it.
| Situation | What to do |
|---|---|
| Spawn an entity that should face right | Set FlipX = true on SpriteRendererComponent (default is false = left-facing as authored) |
Custom AI / chase using MovementComponent:MoveToDirection | Update FlipX on direction change: sprite.FlipX = velocity.x > 0 (right ⇒ flip) |
| Monster model / monster collider alignment | Invert TransformComponent.Scale.x instead of FlipX so the sprite and collider stay aligned; see `msw-general/references/monster.md` |
Native AIChaseComponent / AIWanderComponent | Engine flips automatically based on movement — do nothing |
Top-down (RectTile) movement | Decide per-axis: usually flip when dx > 0; sprites with up/down frames need the StateAnimationComponent action set instead |
_EffectService:PlayEffect(...) should face right | Pass FlipX = true in the options table |
| Player-attached effect must follow the player's facing | Use SyncFlip = true in PlayEffect options, or read PlayerControllerComponent.LookDirectionX |
| Resource is authored facing right (rare) | Inspect payload.thumbnail via GET /v3/resources/{ruid} and invert the rule for that asset |
-- Custom side-view chase: flip sprite to match movement direction
local sprite = self.Entity.SpriteRendererComponent
local selfX = self.Entity.TransformComponent.WorldPosition.x
local dx = targetPos.x - selfX
if dx ~= 0 then
sprite.FlipX = dx > 0 -- target on the right → flip
endSanity check — the left-facing convention is not contractual. Openpayload.thumbnailfromGET /v3/resources/{ruid}to confirm.
>
Do not use `TransformComponent.Scale.x` as a general renderer flip — for players / effects / non-monster renderers, useSpriteRendererComponent.FlipX. Monster exception: monster models should invertTransformComponent.Scale.xso the sprite and collider stay aligned. Related: `msw-combat-system/SKILL.md` "Direction check ★", `msw-general/references/monster.md`.
---
Shared Tips
1. Keyword choice — Use the exact name if you know it; natural-language Korean/English also works. 2. Adjust the page-size parameter — the name differs per endpoint (topK for search/similar, limit for list/packs, count for random). This skill's default is 3 (see the "Result count" table above). Keep it at 3 for precise lookups; increase to 10+ (or 50–100 for avatar broad-browse) only when wider exploration is required. 3. When search fails:
- Document search fails → read
.d.mluadirectly. - Resource search fails → retry with synonyms or a different category; browse with
listResources(...)(CLI:list) by type/category. POSTreturns{"detail":"There was an error parsing the body"}→ you bypassed
the wrapper and sent JSON inline via curl -d '{...}'. Switch to msw_resource_api.cjs (or replicate its UTF-8 raw-body POST pattern) as described in Section 2. 4. Composite queries are allowed — e.g. AttackComponent CalcCritical for docs, red slime jump for resources. 5. No guessing — Never guess API names, RUIDs, or enum values; always confirm via search or references.
Avatar Lookup
Search avatar costume items, browse the full avatar catalog, look up default parts, and inspect item details.
**All examples in this file go through the Node.js wrapper
scripts/msw_resource_api.cjs.** Use it either as a CLI from ashell or via require(...) inside Node.js — never assemble curl commandsby hand.
---
Searching Costume Items — POST /v3/search/resources
Costume items (hats, coats, shoes, weapons, …) are searched through the general resource-search endpoint with resourceTypeFilter: ["avataritem"]. The wrapper exposes this as searchAvatarItems (CLI: search-avatar) and hard-codes that filter, so you only need to pass the query (and optionally a slot category).
# CLI — narrow to a specific slot with --category
node scripts/msw_resource_api.cjs \
search-avatar "early dismissal" --topK 3 --category shoes// Node.js
const { searchAvatarItems } = require('./scripts/msw_resource_api.cjs');
const result = await searchAvatarItems("early dismissal", {
topK: 3,
categoryFilter: ["shoes"],
});Each result has type: "avataritem" and a category matching the avatar slot (cap, coat, pants, shoes, weapon, …). Use the id (RUID) to assign to the corresponding Custom*Equip property — see the slot mapping table in the msw-avatar skill.
**For full request/response details (fields likedname,score,hasEmbedding,
payload.color_hex,categoryFilterslot list, pagination vianextOffset/offset),
see the "Avatar Item Search" section in `references/resource/search.md`.**
---
GET /v3/avatars
List all avatar items (server-cached). Best for browsing tabs without a query; for keyword search use searchAvatarItems.
Usage
# CLI — canonical-only (default; deduped by color/shape variant)
node scripts/msw_resource_api.cjs avatars
# Include all variants
node scripts/msw_resource_api.cjs avatars --no-canonical-only// Node.js
const { listAvatars } = require('./scripts/msw_resource_api.cjs');
const avatars = await listAvatars({ canonicalOnly: true });| Query param (server) | Wrapper arg / CLI flag | Type | Description |
|---|---|---|---|
canonicalOnly | canonicalOnly / --no-canonical-only | bool | Server default true (variant groups deduped to representative) |
Response
{
"items": [
{
"ruid": "d9e9948624a54255b079df8dba096f47",
"category": "coat",
"names": {"ko": ["Yellow Frill Sleeveless"]},
"dname": "coat-541",
"group_id": "coat:757587b9bf92",
"color_hex": "#eeac19",
"group_size": 2,
"group_canonical": false,
"group_members": [...]
}
],
"nextOffset": null,
"total": null
}---
GET /v3/avatars/defaults
Fetch the default avatar body / head RUIDs.
Usage
# CLI
node scripts/msw_resource_api.cjs \
avatar-defaults// Node.js
const { getAvatarDefaults } = require('./scripts/msw_resource_api.cjs');
const defaults = await getAvatarDefaults();Response
{
"body": "body_ruid_32hex",
"head": "head_ruid_32hex"
}body and head are the base avatar parts used with costume item slots.
---
Inspecting an avatar item — use GET /v3/resources/{ruid}
There is no `/v3/avatars/{ruid}` endpoint. Avatar item details
live behind the same GET /v3/resources/{ruid} endpoint that returnssprites, animationclips, and resource_packs.
Usage
# CLI
node scripts/msw_resource_api.cjs \
get ITEM_RUID// Node.js
const { getResource } = require('./scripts/msw_resource_api.cjs');
const item = await getResource("ITEM_RUID");Response (avataritem branch)
{
"id": "71ce85c4acf04770949b7a55488974c2",
"type": "avataritem",
"category": "cap",
"names": {"ko": ["Orange Mushroom Beanie"]},
"dname": "cap-895",
"payload": {
"color_hex": "#ed8316",
"group_size": 2,
"group_canonical": false,
"group_members": [...],
"group_id": "cap:..."
}
}Workflows
Costume item search → application
1. searchAvatarItems("...", { topK: N, categoryFilter: [slot] }) → obtain RUID 2. Use the slot mapping table in the msw-avatar skill to assign the RUID to the correct Custom*Equip property 3. Edit ./Global/DefaultPlayer.model or the relevant .map file → refresh
Inspecting avatar item details
1. searchAvatarItems("...") → search for a costume item and get its RUID 2. getResource(ruid) → inspect color_hex / group meta / variants
Listing, Random Recommendations, and Pack Lookups
Browse resources by type/category, get random recommendations, and find which resource packs include a given RUID.
**All examples in this file go through the Node.js wrapper
scripts/msw_resource_api.cjs.** Use it either as a CLI from ashell or via require(...) inside Node.js — never assemble curl commandsby hand.
GET /v3/resources
Qdrant Scroll over all resources with embeddings. Supports filters and opaque-string cursor pagination.
Usage
# CLI — list 3 monster sprites
node scripts/msw_resource_api.cjs \
list --resource-type sprite --category mob --limit 3// Node.js
const { listResources } = require('./scripts/msw_resource_api.cjs');
const result = await listResources({
resourceTypeFilter: ["sprite"],
categoryFilter: ["mob"],
limit: 3,
});
// Next page:
const nextPage = await listResources({
resourceTypeFilter: ["sprite"],
categoryFilter: ["mob"],
limit: 3,
offset: result.nextOffset,
});| Query param (server) | Wrapper arg / CLI flag | Type | Description |
|---|---|---|---|
resourceTypeFilter | resourceTypeFilter / --resource-type | string[] | sprite, animationclip, resource_pack, bgm, voice, effect (all three are audio), avataritem |
categoryFilter | categoryFilter / --category | string[] | mob, npc, item, skill, object, background, foothold, rope, ladder, etc (or avatar slot). `map` / `effect` / `ui` are NOT valid — they return zero items. See `SKILL.md` "Categories" |
limit | limit / --limit | int (1–100) | Page size (server default 50 / this skill's recommended default 3) |
offset | offset / --offset | string | Opaque cursor returned in nextOffset. Omit on the first page — sending 0 would be treated as a cursor and return empty. |
canonicalOnly | canonicalOnly / --canonical-only | bool | Server default true |
widthMin / widthMax / heightMin / heightMax | widthMin/Max, heightMin/Max | int | Sprite/animationclip size filter |
lengthMin / lengthMax | lengthMin/Max | float | Sound length filter (seconds) |
**Earlier wrapper usedtype/category(singular, string) and
offset=0 (int).** Both are wrong: the server expects array filtersnamedresourceTypeFilter/categoryFilter, andoffsetis an
opaque string cursor (null for the first page).Response
{
"items": [
{
"id": "RUID",
"type": "sprite",
"category": "mob",
"names": {"ko": ["초록버섯"], "en": ["Green Mushroom"]},
"payload": {
"width": 64,
"height": 64,
"thumbnail": "https://..."
}
}
],
"nextOffset": "0008f952-65b8-56bc-9023-98f3eeb28730",
"total": null
}nextOffset is the cursor for the next page (pass it back in offset). Reaches null at the end.
---
GET /v3/resources/random
Random resource recommendation — useful for inspiration or when you just need a sample.
Usage
# CLI — 3 random monster sprites
node scripts/msw_resource_api.cjs \
random --resource-type sprite --category mob --count 3
# 3 random voice clips
node scripts/msw_resource_api.cjs \
random --resource-type voice --count 3
# Fully random with no filters (always specify count explicitly — default 3)
node scripts/msw_resource_api.cjs \
random --count 3// Node.js
const { randomResources } = require('./scripts/msw_resource_api.cjs');
const result = await randomResources({
resourceTypeFilter: ["sprite"],
categoryFilter: ["mob"],
count: 3,
});| Query param (server) | Wrapper arg / CLI flag | Type | Description |
|---|---|---|---|
resourceTypeFilter | resourceTypeFilter / --resource-type | string[] | Resource type filter |
categoryFilter | categoryFilter / --category | string[] | Category filter |
count | count / --count | int (1–100) | Number of results (server default 20 / this skill's recommended default 3) |
canonicalOnly | canonicalOnly / --canonical-only | bool | Server default true |
widthMin/Max, heightMin/Max, lengthMin/Max | wrapper option keys | int / float | Same shape as listResources |
Earlier wrapper sent `limit` / `type` / `category` — the server's
parameters arecount/resourceTypeFilter/categoryFilter.
Response
Same shape as the listing endpoint (items + nextOffset + total).
---
GET /v3/resources/packs/{ruid}
List resource packs that contain the given RUID. The path parameter is a 32-char-hex RUID, NOT a pack id.
Looking up a pack's own contents? Use getResource(packId)(i.e. GET /v3/resources/{packId}) instead — that endpoint returnsthe pack with each payload.elements[*] already populated.Usage
# CLI — find packs that include this animationclip RUID
node scripts/msw_resource_api.cjs \
packs e5fff311269b464984a9b7885a6401e7 --limit 3// Node.js
const { findPacksContaining } = require('./scripts/msw_resource_api.cjs');
const packs = await findPacksContaining("e5fff311269b464984a9b7885a6401e7", { limit: 3 });| Param (server) | Wrapper arg / CLI flag | Type | Description |
|---|---|---|---|
id (path) | ruid (positional) | string | 32-char-hex RUID to search for |
limit | limit / --limit | int (1–100) | Page size (server default 50 / this skill's default 3) |
offset | offset / --offset | string | Opaque cursor (nextOffset from previous page) |
Response
{
"items": [
{
"id": "npc/9072309.img",
"type": "resource_pack",
"category": "npc",
"names": {"ko": ["주황버섯"]},
"assetGuid": "304aba4bea874f948a7548c0d8e393f5",
"payload": {
"elements": [
{
"resource_type": "animationclip",
"ruid": "0a4cee60c89a42bea0d52fd8df00f17c",
"rel_path": "move"
},
{
"resource_type": "animationclip",
"ruid": "e5fff311269b464984a9b7885a6401e7",
"rel_path": "stand"
}
]
}
}
]
}Pack contents (use getResource instead)
To fetch a pack's full contents — same elements array but with each element's payload also filled in — call getResource(packId):
const { getResource } = require('./scripts/msw_resource_api.cjs');
const pack = await getResource("npc/1013617.img");
for (const element of pack.payload.elements) {
// element has resource_type, ruid, rel_path, and payload (width/height/thumbnail/...)
}A resource pack usually consists of multiple animations (stand, walk, attack, etc.), sounds, and per-frame sprites. The RUID of an individual element can be used directly in fields like SpriteRendererComponent.SpriteRUID.
Resource Details & Tags
Fetch details for a single or multiple resources, and inspect AI-generated multilingual tags.
**All examples in this file go through the Node.js wrapper
scripts/msw_resource_api.cjs.** Use it either as a CLI from ashell or via require(...) inside Node.js — never assemble curl commandsby hand.
GET /v3/resources/{ruid}
Fetch detail for a single resource.
Usage
# CLI
node scripts/msw_resource_api.cjs \
get 0017da7385e04bc4b2ddbe5949b4b462// Node.js
const { getResource } = require('./scripts/msw_resource_api.cjs');
const detail = await getResource("0017da7385e04bc4b2ddbe5949b4b462");| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| ruid | path | string | O | Resource RUID (32-char hex) |
Response
{
"id": "0017da7385e04bc4b2ddbe5949b4b462",
"type": "sprite",
"category": "mob",
"names": {
"ko": ["초록버섯"],
"en": ["Green Mushroom"]
},
"assetGuid": "304aba4bea874f948a7548c0d8e393f5",
"payload": {
"width": 64,
"height": 64,
"pivot": {"x": 32, "y": 50},
"thumbnail": "https://...",
"frames": [
{"filename": "frame0.png", "delay": 100}
],
"elements": [
{
"resource_type": "animationclip",
"rel_path": "mob/stand",
"ruid": "element_ruid"
}
]
}
}Key payload Fields
| Field | Type | Description |
|---|---|---|
width, height | int | Image size (pixels) |
pivot | {x, y} | Sprite anchor coordinates |
thumbnail | string | Thumbnail image URL |
frames | array | Animation frame list (animationclip) |
elements | array | Resource pack components (resource_pack) |
framesexists only onanimationclipelementsexists only onresource_packpivotexists onsprite
---
POST /v3/resources/batch
Fetch multiple RUIDs in a single request.
Usage
# CLI — pass RUIDs as space-separated positional arguments
node scripts/msw_resource_api.cjs \
batch 0017da7385e04bc4b2ddbe5949b4b462 abc123def456789012345678abcdef01// Node.js
const { getResourcesBatch } = require('./scripts/msw_resource_api.cjs');
const resources = await getResourcesBatch([
"0017da7385e04bc4b2ddbe5949b4b462",
"abc123def456789012345678abcdef01",
]);| Field | Type | Required | Description |
|---|---|---|---|
| ids | string[] | O | Array of RUIDs |
Response
An array of resource objects. Each element has the same structure as the single-resource response.
---
GET /v3/resources/tags/{ruid}
Fetch AI-generated multilingual tags for a resource — including description, keywords, and per-language tags.
Usage
# CLI
node scripts/msw_resource_api.cjs \
tags 0017da7385e04bc4b2ddbe5949b4b462// Node.js
const { getResourceTags } = require('./scripts/msw_resource_api.cjs');
const tags = await getResourceTags("0017da7385e04bc4b2ddbe5949b4b462");| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| ruid | path | string | O | Resource RUID (32-char hex) |
Response
{
"tags": {
"ko": ["초록", "버섯", "몬스터", "슬라임"],
"en": ["green", "mushroom", "monster", "slime"],
"ja": ["緑", "キノコ", "モンスター"],
"zh": ["绿色", "蘑菇", "怪物"],
"description": "A green mushroom monster from MapleStory",
"keywords": ["green_mushroom", "mob", "monster"]
}
}| Field | Description |
|---|---|
tags.ko/en/ja/zh | Tag array per language |
tags.description | Resource description (English) |
tags.keywords | Search keywords |
Tags are generated automatically by AI and describe the resource's visual traits and intended use. They are useful for similar-resource search and for filtering.
Semantic Search & Similar Resources
Search MSW resources by natural language, and find resources similar to a given one.
**All examples in this file go through the Node.js wrapper
scripts/msw_resource_api.cjs.** Use it either as a CLI from ashell or via require(...) inside Node.js — never assemble curl commandsby hand. The wrapper sends UTF-8 JSON bodies directly, so non-ASCII queries
(Korean / Japanese / Chinese / emoji) are safe and the
{"detail":"There was an error parsing the body"} failure mode isimpossible to trigger.
POST /v3/search/resources
Natural-language semantic search. Supports queries in Korean, English, Japanese, and Chinese.
Usage
# CLI — defaults: topK=3, --resource-type / --category optional
node scripts/msw_resource_api.cjs \
search "orange mushroom" --resource-type resource_pack --category npc --topK 3// Node.js — require and call
const { searchResources } = require('./scripts/msw_resource_api.cjs');
const result = await searchResources("orange mushroom", {
resourceTypeFilter: ["resource_pack"],
categoryFilter: ["npc"],
topK: 3,
});Use require(...) with a relative path (e.g.require('./scripts/msw_resource_api.cjs')) when calling from a sibling file —the wrapper is a single-file CommonJS drop-in with zero dependencies.
| Body field (server) | Wrapper arg / CLI flag | Type | Required | Description |
|---|---|---|---|---|
query | query (positional) | string | O | Search term (natural language, RUID, or pack ID — exact-match patterns trigger ID lookup) |
resourceTypeFilter | resourceTypeFilter / --resource-type | string[] | - | sprite, animationclip, resource_pack, bgm, voice, effect (all three are audio), avataritem |
categoryFilter | categoryFilter / --category | string[] | - | mob, npc, item, skill, object, background, foothold, rope, ladder, etc (or avatar slot when resourceTypeFilter=["avataritem"]). `map` / `effect` / `ui` are NOT valid — see `SKILL.md` "Categories" |
topK | topK / --topK | int (1–100) | - | Number of results (server default 20 / this skill's recommended default 3 — always send explicitly) |
offset | offset / --offset | int (0–5000) | - | Result offset; default 0 |
canonicalOnly | canonicalOnly / --canonical-only | bool | - | Server default true (sprite/animationclip/avataritem dedup) |
widthMin / widthMax | widthMin / widthMax / --width-min/max | int | - | Sprite/animationclip width filter |
heightMin / heightMax | heightMin / heightMax / --height-min/max | int | - | Sprite/animationclip height filter |
lengthMin / lengthMax | lengthMin / lengthMax / --length-min/max | float | - | Sound length filter (seconds) |
**The legacy field namestypes/categories/limitare silently
ignored by the server.** Always use the canonical names above —
the wrapper takes care of this for you.
Avatar-item search uses the same endpoint with
resourceTypeFilter=["avataritem"] already filled in. Use thesearchAvatarItemswrapper (CLI:search-avatar) — see "Avatar
Item Search" below.
Response
{
"results": [
{
"id": "0017da7385e04bc4b2ddbe5949b4b462",
"type": "resource_pack",
"category": "mob",
"names": {
"ko": ["초록버섯"],
"en": ["Green Mushroom"]
},
"dname": "Green Mushroom",
"score": 0.9234,
"assetGuid": "304aba4bea874f948a7548c0d8e393f5",
"payload": {
"width": 64,
"height": 64,
"thumbnail": "https://...",
"elements": [
{
"resource_type": "animationclip",
"rel_path": "mob/9833419.img/move",
"ruid": "abc123..."
}
]
}
}
],
"nextOffset": 5
}Key Response Fields
| Field | Description |
|---|---|
id | RUID — 32-char hex resource identifier |
type | Resource type |
category | Category |
names | Multilingual names (prefer ko > en) |
score | Semantic similarity score (0–1) |
assetGuid | Unity asset GUID (used in spawn_preset, may be missing) |
payload.thumbnail | Thumbnail image URL |
payload.width/height | Image dimensions |
payload.elements | Resource pack components (sprite, animation, sound) |
nextOffset | Next page offset (integer; pass it back as offset to paginate) |
Choosing resourceTypeFilter
- Default:
resource_pack— a finished asset bundling sprites + animations + sounds; suitable for most searches - Sound / audio:
bgm(background music) /voice(NPC voice) /effect(sound effect — not visual) - Individual sprite:
sprite - Individual animation:
animationclip - Visual effect / particle / hit FX:
spriteoranimationclip+categoryFilter: ["skill","mob","etc"]— there is no visualeffectresource_type or category - Avatar costume item:
avataritem(or just callsearchAvatarItems)
Using categoryFilter
| User phrase | category |
|---|---|
| Monster, mob | mob |
| NPC | npc |
| Item | item |
| Skill resource / skill effect | skill |
| Tree / rock / map object / decoration | object |
| Background / map tile / scenery / BGM | background |
| Walkable platform | foothold |
| Rope | rope |
| Ladder | ladder |
| Uncategorized / misc | etc |
| Visual effect / particle | use categoryFilter: ["skill","mob","etc"] with resourceTypeFilter: ["sprite","animationclip"] — no dedicated effect category |
| Sound effect (audio) | use resourceTypeFilter: ["effect"] — effect is an audio resource_type, not a category |
| UI element | not indexed under a ui category — search sprite + category: "etc" or by direct name |
Search Tips
- If you miss on the first try, retry with synonyms / English / Korean:
- "tree" → "forest" → "forest background" → "plant"
- "running monster" → "moving monster" → "moving enemy"
- You can also pass a RUID directly as
query.
---
Avatar Item Search (resourceTypeFilter: ["avataritem"])
The same POST /v3/search/resources endpoint also covers avatar costume items (hats, coats, shoes, weapons, etc.). This is the only supported way to obtain avatar item RUIDs by natural-language search — the legacy full-list endpoint is no longer used.
The wrapper hard-codes resourceTypeFilter=["avataritem"]. You can narrow the search to a specific avatar slot by passing categoryFilter: ["cap", ...] (CLI: --category cap).
Usage
# CLI
node scripts/msw_resource_api.cjs \
search-avatar "early dismissal" --topK 3 --category shoes// Node.js
const { searchAvatarItems } = require('./scripts/msw_resource_api.cjs');
const result = await searchAvatarItems("early dismissal", {
topK: 3,
categoryFilter: ["shoes"],
});| Body field (server) | Wrapper arg / CLI flag | Type | Required | Description |
|---|---|---|---|---|
query | query (positional) | string | O | Search term (Korean / English / Japanese / Chinese) |
topK | topK / --topK | int (1–100) | - | Page size (server default 20 / this skill's recommended default 3) |
offset | offset / --offset | int (0–5000) | - | Pagination offset (default 0) |
categoryFilter | categoryFilter / --category | string[] | - | Avatar slot: cap, cape, coat, longcoat, pants, shoes, glove, hair, face, weapon, twohandweapon, subweapon, shield, … |
canonicalOnly | canonicalOnly / --canonical-only | bool | - | Server default true (color/shape variants deduped) |
The wrapper hard-codes resourceTypeFilter=["avataritem"], so you do notneed to pass it explicitly.
Response
{
"query": "early dismissal",
"results": [
{
"id": "ac02d9eb84dc4c4197dfce6721c6543c",
"type": "avataritem",
"category": "shoes",
"names": { "ko": ["이른 하교"] },
"dname": "shoes-1141",
"assetGuid": null,
"score": 0.78686994,
"hasEmbedding": true,
"payload": {
"color_hex": "#981125",
"group_size": null,
"group_canonical": null,
"group_members": null,
"group_id": null
}
}
],
"nextOffset": 2,
"exactMatch": false
}Avatar-Specific Response Fields
| Field | Description |
|---|---|
id | RUID — assign to the matching Custom*Equip slot (see slot mapping in the msw-avatar skill) |
type | Always avataritem |
category | Avatar slot / part category (shoes, cap, coat, weapon, …) — drives the slot mapping |
dname | Internal display name (e.g. shoes-1141); useful as a stable secondary key |
assetGuid | Usually null for avatar items — do not depend on it for avatar items |
score | Semantic similarity (0–1) |
hasEmbedding | Whether the item has a vector embedding indexed (filter out false if you want only well-indexed results) |
payload.color_hex | Dominant color in #RRGGBB — handy for color-based filtering |
payload.group_* | Variant grouping (e.g. recolor families). May be null when the item is standalone. |
nextOffset | Pass back as offset to fetch the next page |
exactMatch | true when the query exactly matches an item name |
Tips
- The default
topKis 3 (this skill's convention). Increase to 50–100 only when explicitly browsing widely. categoryin avatar item results aligns with avatar slots (cap,coat,pants,shoes,weapon, …) — use the slot mapping table in themsw-avatarskill to assign to the correctCustom*Equipproperty.payload.color_hexlets you filter results client-side, e.g. "red shoes" → search "shoes", then keep entries whosecolor_hexis reddish.
---
GET /v3/search/resources/similar/{id}
Find resources that are visually or semantically similar to a given resource.
Usage
# CLI
node scripts/msw_resource_api.cjs \
similar 0017da7385e04bc4b2ddbe5949b4b462 --topK 3 --resource-type animationclip// Node.js
const { findSimilarResources } = require('./scripts/msw_resource_api.cjs');
const result = await findSimilarResources("0017da7385e04bc4b2ddbe5949b4b462", {
topK: 3,
resourceTypeFilter: ["animationclip"],
});| Query param (server) | Wrapper arg / CLI flag | Type | Description |
|---|---|---|---|
id (path) | ruid (positional) | string | Source resource RUID (32-char hex) |
topK | topK / --topK | int (1–100) | Number of results (server default 20 / this skill's recommended default 3) |
resourceTypeFilter | resourceTypeFilter / --resource-type | string[] | Optional type narrowing |
categoryFilter | categoryFilter / --category | string[] | Optional category narrowing (incl. avatar slot for avataritem source) |
canonicalOnly | canonicalOnly / --canonical-only | bool | Server default true |
widthMin / widthMax / heightMin / heightMax | widthMin/Max, heightMin/Max | int | Sprite/animationclip size filter |
Earlier wrapper sent limit; the server's parameter is `topK`.Response
An array with the same structure as search results. score represents similarity to the source resource.
#!/usr/bin/env node
/**
* MSW Resource Search API — Node.js (CommonJS) wrapper.
*
* Node.js implementation. Exposes every endpoint of the MSW resource search
* REST API as functions and CLI subcommands.
*
* const api = require('./msw_resource_api');
* const hits = await api.searchResources('orange mushroom', {
* resourceTypeFilter: ['resource_pack'],
* categoryFilter: ['npc'],
* topK: 3,
* });
* const detail = await api.getResource(hits.results[0].id);
*
* CLI:
* node msw_resource_api.cjs search "orange mushroom" \
* --resource-type resource_pack --category npc --topK 3
* node msw_resource_api.cjs get 0017da7385e04bc4b2ddbe5949b4b462
* Design notes:
* - No external dependencies. Uses only Node 18+ built-in `fetch`/`AbortController`.
* - All POST bodies are encoded as UTF-8 JSON and sent with
* `Content-Type: application/json; charset=utf-8`.
* Korean / Japanese / emoji payloads are safe.
* - Default page size for list-style endpoints is 3 (per the SKILL.md convention).
*/
'use strict';
const BASE_URL = 'https://maplestoryworlds-resourcesearch-new.nexon.com/api';
const DEFAULT_TIMEOUT_MS = 15_000; // SKILL.md recommends 15s
const DEFAULT_LIMIT = 3; // skill convention (server defaults are 5/10)
class MswApiError extends Error {
constructor(status, url, body) {
const snippet = typeof body === 'string' ? body.slice(0, 500) : String(body);
super(`MSW API ${status} on ${url}: ${snippet}`);
this.name = 'MswApiError';
this.status = status;
this.url = url;
this.body = body;
}
}
// ---------------------------------------------------------------------------
// Input validation helpers
// ---------------------------------------------------------------------------
const HEX_RUID_RE = /^[0-9a-f]{32}$/i;
const UUID_CURSOR_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/**
* Normalize the `offset` argument for list-style endpoints (`/v3/resources`,
* `/v3/resources/packs/{ruid}`) whose cursor is an opaque UUID string.
*
* - `undefined` / `null` → undefined (first page).
* - Valid UUID cursor → returned as-is.
* - `0` / `"0"` / `""` / `"null"` → undefined (silent: common "first page"
* confusion that would otherwise return zero items).
* - Anything else → undefined, with a stderr warning so the caller learns
* that the value was discarded.
*/
function _normalizeListOffset(offset) {
if (offset === undefined || offset === null) return undefined;
const s = String(offset);
if (UUID_CURSOR_RE.test(s)) return s;
if (s === '0' || s === '' || s === 'null' || s === 'undefined') return undefined;
if (typeof process !== 'undefined' && process.stderr && process.stderr.write) {
process.stderr.write(
`[msw-api warn] offset "${s}" is not a valid cursor `
+ `(expected the UUID string returned in the previous response's nextOffset); `
+ `ignoring and fetching the first page.\n`
);
}
return undefined;
}
// ---------------------------------------------------------------------------
// Low-level HTTP helper
// ---------------------------------------------------------------------------
/**
* Serialize a query object to a URLSearchParams string.
* - null / undefined values are dropped.
* - Arrays / tuples are repeated under the same key (`types=a&types=b`).
*/
function _buildQuery(query) {
if (!query) return '';
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value === null || value === undefined) continue;
if (Array.isArray(value)) {
for (const item of value) {
if (item === null || item === undefined) continue;
params.append(key, String(item));
}
} else {
params.append(key, String(value));
}
}
const s = params.toString();
return s ? `?${s}` : '';
}
async function _request(method, path, { query, body, timeout = DEFAULT_TIMEOUT_MS } = {}) {
const url = BASE_URL + path + _buildQuery(query);
const headers = { Accept: 'application/json' };
let payload;
if (body !== undefined && body !== null) {
payload = Buffer.from(JSON.stringify(body), 'utf8');
headers['Content-Type'] = 'application/json; charset=utf-8';
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
let resp;
try {
resp = await fetch(url, {
method,
headers,
body: payload,
signal: controller.signal,
});
} catch (err) {
clearTimeout(timer);
const reason = err && err.name === 'AbortError'
? `timeout after ${timeout}ms`
: (err && err.message) || String(err);
throw new MswApiError(0, url, reason);
}
clearTimeout(timer);
const text = await resp.text();
if (!resp.ok) {
throw new MswApiError(resp.status, url, text);
}
if (!text) return null;
try {
return JSON.parse(text);
} catch (_e) {
return text;
}
}
const _enc = (s) => encodeURIComponent(s);
// ---------------------------------------------------------------------------
// Section: Semantic Search — POST /v3/search/resources & similar
// ---------------------------------------------------------------------------
/**
* Natural-language semantic search.
* `POST /v3/search/resources` (SearchRequest schema).
* Used to search sprite / animationclip / resource_pack / sound / avataritem.
* For avatar costumes, prefer `searchAvatarItems` — it pins
* `resourceTypeFilter=["avataritem"]` automatically.
*
* Body keys follow the OpenAPI spec verbatim (`topK`, `resourceTypeFilter`,
* `categoryFilter`, …). The legacy wrapper's `limit` / `types` / `categories`
* were silently ignored by the server.
*
* @param {string} query
* @param {object} [opts]
* @returns {Promise<object>}
*/
async function searchResources(query, opts = {}) {
const {
resourceTypeFilter,
categoryFilter,
topK = DEFAULT_LIMIT,
offset = 0,
canonicalOnly,
widthMin, widthMax, heightMin, heightMax,
lengthMin, lengthMax,
compact = true,
} = opts;
const payload = { query, topK, offset };
if (resourceTypeFilter !== undefined) payload.resourceTypeFilter = [...resourceTypeFilter];
if (categoryFilter !== undefined) payload.categoryFilter = [...categoryFilter];
if (canonicalOnly !== undefined && canonicalOnly !== null) payload.canonicalOnly = canonicalOnly;
if (widthMin !== undefined && widthMin !== null) payload.widthMin = widthMin;
if (widthMax !== undefined && widthMax !== null) payload.widthMax = widthMax;
if (heightMin !== undefined && heightMin !== null) payload.heightMin = heightMin;
if (heightMax !== undefined && heightMax !== null) payload.heightMax = heightMax;
if (lengthMin !== undefined && lengthMin !== null) payload.lengthMin = lengthMin;
if (lengthMax !== undefined && lengthMax !== null) payload.lengthMax = lengthMax;
return _request('POST', '/v3/search/resources', {
query: compact ? { compact: 'true' } : undefined,
body: payload,
});
}
/**
* Search avatar costume items (cap, coat, pants, shoes, weapon, …).
* `POST /v3/search/resources` + `resourceTypeFilter=["avataritem"]`.
*/
async function searchAvatarItems(query, opts = {}) {
const {
topK = DEFAULT_LIMIT,
offset = 0,
categoryFilter,
canonicalOnly,
compact = true,
} = opts;
const payload = {
query, topK, offset,
resourceTypeFilter: ['avataritem'],
};
if (categoryFilter !== undefined) payload.categoryFilter = [...categoryFilter];
if (canonicalOnly !== undefined && canonicalOnly !== null) payload.canonicalOnly = canonicalOnly;
return _request('POST', '/v3/search/resources', {
query: compact ? { compact: 'true' } : undefined,
body: payload,
});
}
/**
* Find resources similar to a given RUID.
* `GET /v3/search/resources/similar/{id}`. The server uses `topK`
* (default 20, max 100). The legacy wrapper's `limit` was ignored.
*/
async function findSimilarResources(ruid, opts = {}) {
const {
topK = DEFAULT_LIMIT,
resourceTypeFilter,
categoryFilter,
canonicalOnly,
widthMin, widthMax, heightMin, heightMax,
compact = true,
} = opts;
const query = { topK };
if (resourceTypeFilter !== undefined) query.resourceTypeFilter = [...resourceTypeFilter];
if (categoryFilter !== undefined) query.categoryFilter = [...categoryFilter];
if (canonicalOnly !== undefined && canonicalOnly !== null) query.canonicalOnly = canonicalOnly ? 'true' : 'false';
if (widthMin !== undefined && widthMin !== null) query.widthMin = widthMin;
if (widthMax !== undefined && widthMax !== null) query.widthMax = widthMax;
if (heightMin !== undefined && heightMin !== null) query.heightMin = heightMin;
if (heightMax !== undefined && heightMax !== null) query.heightMax = heightMax;
if (compact) query.compact = 'true';
return _request('GET', `/v3/search/resources/similar/${_enc(ruid)}`, { query });
}
// ---------------------------------------------------------------------------
// Section: Resource Details & Tags
// ---------------------------------------------------------------------------
/** Fetch a single resource's details. `GET /v3/resources/{ruid}`. */
async function getResource(ruid) {
return _request('GET', `/v3/resources/${_enc(ruid)}`);
}
/** Batch-fetch multiple resources. `POST /v3/resources/batch`. */
async function getResourcesBatch(ids) {
return _request('POST', '/v3/resources/batch', { body: { ids: [...ids] } });
}
/** Fetch AI-generated multilingual tags. `GET /v3/resources/tags/{ruid}`. */
async function getResourceTags(ruid) {
return _request('GET', `/v3/resources/tags/${_enc(ruid)}`);
}
// ---------------------------------------------------------------------------
// Section: Browsing — listings, random, and pack details
// ---------------------------------------------------------------------------
/**
* Qdrant Scroll-based resource listing. `GET /v3/resources`.
* `offset` is the `nextOffset` string cursor from the previous response.
* Omit it for the first page.
* Fixes the legacy bug where the wrapper sent `offset=0` (int) and matched 0 items.
*
* Filters are sent under the canonical OpenAPI keys
* `resourceTypeFilter` / `categoryFilter`.
*/
async function listResources(opts = {}) {
const {
resourceTypeFilter,
categoryFilter,
limit = DEFAULT_LIMIT,
offset,
canonicalOnly,
widthMin, widthMax, heightMin, heightMax,
lengthMin, lengthMax,
compact = true,
} = opts;
const query = { limit };
if (resourceTypeFilter !== undefined) query.resourceTypeFilter = [...resourceTypeFilter];
if (categoryFilter !== undefined) query.categoryFilter = [...categoryFilter];
const cursor = _normalizeListOffset(offset);
if (cursor !== undefined) query.offset = cursor;
if (canonicalOnly !== undefined && canonicalOnly !== null) query.canonicalOnly = canonicalOnly ? 'true' : 'false';
if (widthMin !== undefined && widthMin !== null) query.widthMin = widthMin;
if (widthMax !== undefined && widthMax !== null) query.widthMax = widthMax;
if (heightMin !== undefined && heightMin !== null) query.heightMin = heightMin;
if (heightMax !== undefined && heightMax !== null) query.heightMax = heightMax;
if (lengthMin !== undefined && lengthMin !== null) query.lengthMin = lengthMin;
if (lengthMax !== undefined && lengthMax !== null) query.lengthMax = lengthMax;
if (compact) query.compact = 'true';
return _request('GET', '/v3/resources', { query });
}
/**
* Random resource recommendations. `GET /v3/resources/random`.
* The server uses `count` (NOT `limit`) along with
* `resourceTypeFilter` / `categoryFilter`.
*/
async function randomResources(opts = {}) {
const {
resourceTypeFilter,
categoryFilter,
count = DEFAULT_LIMIT,
canonicalOnly,
widthMin, widthMax, heightMin, heightMax,
lengthMin, lengthMax,
compact = true,
} = opts;
const query = { count };
if (resourceTypeFilter !== undefined) query.resourceTypeFilter = [...resourceTypeFilter];
if (categoryFilter !== undefined) query.categoryFilter = [...categoryFilter];
if (canonicalOnly !== undefined && canonicalOnly !== null) query.canonicalOnly = canonicalOnly ? 'true' : 'false';
if (widthMin !== undefined && widthMin !== null) query.widthMin = widthMin;
if (widthMax !== undefined && widthMax !== null) query.widthMax = widthMax;
if (heightMin !== undefined && heightMin !== null) query.heightMin = heightMin;
if (heightMax !== undefined && heightMax !== null) query.heightMax = heightMax;
if (lengthMin !== undefined && lengthMin !== null) query.lengthMin = lengthMin;
if (lengthMax !== undefined && lengthMax !== null) query.lengthMax = lengthMax;
if (compact) query.compact = 'true';
return _request('GET', '/v3/resources/random', { query });
}
/**
* List resource packs that contain a given RUID.
* `GET /v3/resources/packs/{id}` — the path parameter is a 32-hex RUID
* (NOT a pack id). The server returns packs whose `payload.elements` include that RUID.
*
* To fetch a pack's own metadata + populated elements, use `getResource(packId)`
* — that endpoint fills in the payload of each element and returns it.
*/
async function findPacksContaining(ruid, opts = {}) {
if (!HEX_RUID_RE.test(String(ruid || ''))) {
throw new Error(
`'packs' expects a 32-hex RUID, got "${ruid}". `
+ `If you want a pack's contents, use getResource(packId) (CLI: 'get <packId>') instead — `
+ `that endpoint returns the pack with each payload.elements[*] populated.`
);
}
const { limit = DEFAULT_LIMIT, offset, compact = true } = opts;
const query = { limit };
const cursor = _normalizeListOffset(offset);
if (cursor !== undefined) query.offset = cursor;
if (compact) query.compact = 'true';
return _request('GET', `/v3/resources/packs/${_enc(ruid)}`, { query });
}
// ---------------------------------------------------------------------------
// Section: Avatar — listings and defaults
// ---------------------------------------------------------------------------
/**
* List every avatar item (server-cached).
* `GET /v3/avatars`. For keyword search, use `searchAvatarItems`.
*/
async function listAvatars({ canonicalOnly = true } = {}) {
return _request('GET', '/v3/avatars', {
query: { canonicalOnly: canonicalOnly ? 'true' : 'false' },
});
}
/** Fetch the default body / head RUIDs. `GET /v3/avatars/defaults`. */
async function getAvatarDefaults() {
return _request('GET', '/v3/avatars/defaults');
}
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
/** Tiny argv parser. Does not port argparse wholesale — only what we need. */
function _parseArgs(argv, spec) {
// spec: { positional: [{name, nargs?: '+'|undefined}], options: { flag: {dest, type, nargs?, const?, default?} } }
const result = {};
for (const [, opt] of Object.entries(spec.options || {})) {
if (opt.default !== undefined) result[opt.dest] = opt.default;
}
const positionals = [];
let i = 0;
while (i < argv.length) {
const tok = argv[i];
if (tok.startsWith('--')) {
const opt = spec.options && spec.options[tok];
if (!opt) throw new Error(`unknown option: ${tok}`);
if (opt.nargs === '+') {
const values = [];
i += 1;
while (i < argv.length && !argv[i].startsWith('--')) {
values.push(argv[i]);
i += 1;
}
if (values.length === 0) throw new Error(`option ${tok} requires at least one value`);
result[opt.dest] = opt.type === 'int' ? values.map((v) => parseInt(v, 10))
: opt.type === 'float' ? values.map((v) => parseFloat(v))
: values;
continue;
}
if (opt.const !== undefined) {
result[opt.dest] = opt.const;
i += 1;
continue;
}
const v = argv[i + 1];
if (v === undefined) throw new Error(`option ${tok} requires a value`);
result[opt.dest] = opt.type === 'int' ? parseInt(v, 10)
: opt.type === 'float' ? parseFloat(v)
: v;
i += 2;
continue;
}
positionals.push(tok);
i += 1;
}
let pi = 0;
for (const p of spec.positional || []) {
if (p.nargs === '+') {
if (pi >= positionals.length) throw new Error(`missing positional: ${p.name}`);
result[p.name] = positionals.slice(pi);
pi = positionals.length;
} else {
if (pi >= positionals.length) throw new Error(`missing positional: ${p.name}`);
result[p.name] = positionals[pi];
pi += 1;
}
}
return result;
}
function _printJson(value) {
if (value === null || value === undefined) return;
if (typeof value === 'object') {
process.stdout.write(JSON.stringify(value, null, 2) + '\n');
} else {
process.stdout.write(String(value) + '\n');
}
}
const COMMON_FILTERS = {
'--resource-type': { dest: 'resourceTypeFilter', nargs: '+' },
'--category': { dest: 'categoryFilter', nargs: '+' },
'--canonical-only': { dest: 'canonicalOnly', const: true },
'--no-canonical-only': { dest: 'canonicalOnly', const: false },
'--no-compact': { dest: 'compact', const: false },
'--width-min': { dest: 'widthMin', type: 'int' },
'--width-max': { dest: 'widthMax', type: 'int' },
'--height-min': { dest: 'heightMin', type: 'int' },
'--height-max': { dest: 'heightMax', type: 'int' },
'--length-min': { dest: 'lengthMin', type: 'float' },
'--length-max': { dest: 'lengthMax', type: 'float' },
};
const CLI_USAGE = `\
MSW Resource Search API CLI
Usage:
node msw_resource_api.cjs <command> [args]
Commands:
search <query> [--resource-type ...] [--category ...] [--topK N] [--offset N]
[--canonical-only|--no-canonical-only] [--width-min N] ...
[--no-compact]
search-avatar <query> [--topK N] [--offset N] [--category ...] [--no-compact]
similar <ruid> [--topK N] [--resource-type ...] [--category ...] [--no-compact]
get <ruid>
batch <id1> <id2> ...
tags <ruid>
list [--resource-type ...] [--category ...] [--limit N] [--offset CURSOR] ...
random [--resource-type ...] [--category ...] [--count N] ...
packs <ruid> [--limit N] [--offset CURSOR] [--no-compact]
avatars [--no-canonical-only]
avatar-defaults
`;
const CLI_HANDLERS = {
search: async (argv) => {
const a = _parseArgs(argv, {
positional: [{ name: 'query' }],
options: {
...COMMON_FILTERS,
'--topK': { dest: 'topK', type: 'int', default: DEFAULT_LIMIT },
'--offset': { dest: 'offset', type: 'int', default: 0 },
},
});
return searchResources(a.query, {
resourceTypeFilter: a.resourceTypeFilter,
categoryFilter: a.categoryFilter,
topK: a.topK, offset: a.offset,
canonicalOnly: a.canonicalOnly,
widthMin: a.widthMin, widthMax: a.widthMax,
heightMin: a.heightMin, heightMax: a.heightMax,
lengthMin: a.lengthMin, lengthMax: a.lengthMax,
compact: a.compact !== false,
});
},
'search-avatar': async (argv) => {
const a = _parseArgs(argv, {
positional: [{ name: 'query' }],
options: {
'--category': { dest: 'categoryFilter', nargs: '+' },
'--canonical-only': { dest: 'canonicalOnly', const: true },
'--no-canonical-only': { dest: 'canonicalOnly', const: false },
'--no-compact': { dest: 'compact', const: false },
'--topK': { dest: 'topK', type: 'int', default: DEFAULT_LIMIT },
'--offset': { dest: 'offset', type: 'int', default: 0 },
},
});
return searchAvatarItems(a.query, {
topK: a.topK, offset: a.offset,
categoryFilter: a.categoryFilter,
canonicalOnly: a.canonicalOnly,
compact: a.compact !== false,
});
},
similar: async (argv) => {
const a = _parseArgs(argv, {
positional: [{ name: 'ruid' }],
options: {
'--resource-type': { dest: 'resourceTypeFilter', nargs: '+' },
'--category': { dest: 'categoryFilter', nargs: '+' },
'--canonical-only': { dest: 'canonicalOnly', const: true },
'--no-canonical-only': { dest: 'canonicalOnly', const: false },
'--no-compact': { dest: 'compact', const: false },
'--topK': { dest: 'topK', type: 'int', default: DEFAULT_LIMIT },
},
});
return findSimilarResources(a.ruid, {
topK: a.topK,
resourceTypeFilter: a.resourceTypeFilter,
categoryFilter: a.categoryFilter,
canonicalOnly: a.canonicalOnly,
compact: a.compact !== false,
});
},
get: async (argv) => {
const a = _parseArgs(argv, { positional: [{ name: 'ruid' }] });
return getResource(a.ruid);
},
batch: async (argv) => {
const a = _parseArgs(argv, { positional: [{ name: 'ids', nargs: '+' }] });
return getResourcesBatch(a.ids);
},
tags: async (argv) => {
const a = _parseArgs(argv, { positional: [{ name: 'ruid' }] });
return getResourceTags(a.ruid);
},
list: async (argv) => {
const a = _parseArgs(argv, {
options: {
...COMMON_FILTERS,
'--limit': { dest: 'limit', type: 'int', default: DEFAULT_LIMIT },
'--offset': { dest: 'offset' }, // string cursor
},
});
return listResources({
resourceTypeFilter: a.resourceTypeFilter,
categoryFilter: a.categoryFilter,
limit: a.limit, offset: a.offset,
canonicalOnly: a.canonicalOnly,
widthMin: a.widthMin, widthMax: a.widthMax,
heightMin: a.heightMin, heightMax: a.heightMax,
lengthMin: a.lengthMin, lengthMax: a.lengthMax,
compact: a.compact !== false,
});
},
random: async (argv) => {
const a = _parseArgs(argv, {
options: {
...COMMON_FILTERS,
'--count': { dest: 'count', type: 'int', default: DEFAULT_LIMIT },
},
});
return randomResources({
resourceTypeFilter: a.resourceTypeFilter,
categoryFilter: a.categoryFilter,
count: a.count,
canonicalOnly: a.canonicalOnly,
widthMin: a.widthMin, widthMax: a.widthMax,
heightMin: a.heightMin, heightMax: a.heightMax,
lengthMin: a.lengthMin, lengthMax: a.lengthMax,
compact: a.compact !== false,
});
},
packs: async (argv) => {
const a = _parseArgs(argv, {
positional: [{ name: 'ruid' }],
options: {
'--limit': { dest: 'limit', type: 'int', default: DEFAULT_LIMIT },
'--offset': { dest: 'offset' },
'--no-compact': { dest: 'compact', const: false },
},
});
return findPacksContaining(a.ruid, {
limit: a.limit, offset: a.offset,
compact: a.compact !== false,
});
},
avatars: async (argv) => {
const a = _parseArgs(argv, {
options: { '--no-canonical-only': { dest: 'canonicalOnly', const: false } },
});
return listAvatars({ canonicalOnly: a.canonicalOnly !== false });
},
'avatar-defaults': async () => getAvatarDefaults(),
};
async function main(argv = process.argv.slice(2)) {
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
process.stdout.write(CLI_USAGE);
return 0;
}
const [cmd, ...rest] = argv;
const handler = CLI_HANDLERS[cmd];
if (!handler) {
process.stderr.write(`unknown command: ${cmd}\n\n${CLI_USAGE}`);
return 2;
}
try {
const value = await handler(rest);
_printJson(value);
return 0;
} catch (err) {
if (err instanceof MswApiError) {
process.stderr.write(`[msw-api error] ${err.message}\n`);
return 2;
}
process.stderr.write(`[error] ${err && err.message ? err.message : String(err)}\n`);
return 2;
}
}
module.exports = {
BASE_URL,
DEFAULT_TIMEOUT_MS,
DEFAULT_LIMIT,
MswApiError,
searchResources,
searchAvatarItems,
findSimilarResources,
getResource,
getResourcesBatch,
getResourceTags,
listResources,
randomResources,
findPacksContaining,
listAvatars,
getAvatarDefaults,
main,
};
if (require.main === module) {
main().then((code) => process.exit(code));
}
Related skills
How it compares
MSW-specific MCP and REST lookup router, not a generic documentation search skill.
FAQ
When should I use document search versus resource search?
Use document search for API examples and guides; use resource search when you need SpriteRUID, animation, sound, or avatar identifiers.
Why start resource search with resource_pack?
A pack bundles sprites, animations, and sounds for one asset, avoiding single-frame or incomplete RUID assignments.
Is Msw Search safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.