game-asset-mcp
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Warn
- process.env — Environment variable access in scripts/verify-install.mjs
- process.env — Environment variable access in src/config.ts
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
MCP server for agentic game-asset generation: reference images to 3D models with PBR textures, plus retexturing meshes you already own.
game-asset-mcp
An MCP server that lets an AI agent produce game-ready 3D assets end to end — reference image, mesh, PBR textures, provenance — and retexture meshes you already own.
Most asset-generation tooling stops at "type a prompt, get a mesh". That is the easy half. The half that actually blocks a project is the mesh you already have: the kitbash you modelled last week, the marketplace prop whose materials are wrong for your art direction, the greybox that needs to look like corroded steel by Friday. texture_existing_asset takes a mesh you supply and gives it new PBR materials without regenerating the geometry you already approved.
Everything is recorded. Every job keeps the prompt, the seed, the provider model version, the provider task id, and a SHA-256 for every downloaded byte — so six months from now you can still answer "what produced this file?"
The server is provider-agnostic by construction. Today it drives Tripo for 3D and Leonardo.Ai for reference images and sound effects, behind three small interfaces (ImageProvider, Model3DProvider, AudioProvider). Adding a provider does not change the tool surface. See docs/architecture.md for why it is built this way.
Requirements
- Node.js >= 18.17 — the server uses global
fetch,FormData,BlobandAbortController. - No native modules, no build toolchain, no database. It runs anywhere Node runs.
- Optional: a local Blender 4.x+ install enables the repair half of
normalize_meshandbatch_prepare_meshes. Every other tool works without it; the tool refuses with instructions when it is absent. On macOS Blender is not onPATH, so setBLENDER_PATHor rely on the bundled/Applications/Blender.appdefault. - At least one provider API key (see Configuration). One is enough — they are validated lazily.
Installation
Install straight from GitHub. Both forms build the TypeScript during install, so you get a runnable game-asset-mcp binary either way.
# run it without installing anything permanently
npx github:theisegoria/game-asset-mcp
# or add it to a project
npm install github:theisegoria/game-asset-mcp
Or work from a clone, which is what you want if you intend to change anything:
git clone https://github.com/theisegoria/game-asset-mcp.git
cd game-asset-mcp
npm install
npm run build # emits dist/
node dist/server.js
Not on npm. There is no
npm install @theisegoria/game-asset-mcp— the package is distributed from GitHub only. Anything telling you otherwise is out of date.
The server speaks MCP over stdio. Started directly in a terminal it will simply sit there waiting for a client to talk to it — that is correct behaviour, not a hang. Logs go to stderr; stdout belongs to the protocol.
Configuration
Set these in your MCP client's env block — see the snippets below. There is no .env loading: the server reads process.env and nothing else, so a .env file on disk does nothing unless your shell or client exports it first.
| Variable | Required | Default | Purpose |
|---|---|---|---|
TRIPO_API_KEY |
for 3D tools | — | Tripo API key. Create one at platform.tripo3d.ai. |
LEONARDO_API_KEY |
for image and audio tools | — | Leonardo.Ai key with API access enabled. One key covers both reference images and sound effects. |
LEONARDO_MODEL_ID |
no | built-in default | Override the default Leonardo image model. A per-call modelId also exists. |
ASSET_OUTPUT_DIR |
no | ./assets/generated |
Where assets and job records are written. Relative to the server's working directory. |
ASSET_MAX_DOWNLOAD_BYTES |
no | 268435456 (256 MiB) |
Hard ceiling on any single download, enforced while streaming. |
ASSET_HTTP_TIMEOUT_MS |
no | 60000 |
Per-request HTTP timeout. |
ASSET_LOG_LEVEL |
no | info |
silent | error | warn | info | debug. |
BLENDER_PATH |
no | auto-detected | Blender executable for normalize_mesh and batch_prepare_meshes. Overrides discovery. |
TRIPO_BASE_URL |
no | Tripo v3 endpoint | Retarget the 3D provider. Must be https://; an http:// value is refused when the provider is first used, not at startup, because providers are constructed lazily. |
LEONARDO_BASE_URL |
no | Leonardo endpoint | Retarget the image/audio provider. Must be https://, refused on first use, same reason. |
ASSET_SPEND_LIMIT_CENTS |
no | unlimited | Session spend ceiling in US cents. Credit-consuming tools refuse once it is reached, before contacting the provider. |
⚠️ Tripo API credits are billed separately from a Tripo Studio subscription
This catches almost everyone. A Tripo Studio web subscription does not fund API calls. They are two different products with two different balances. If you have been happily generating models in the Studio web app and your very first create_3d_asset call comes back rejected for insufficient credits, you have not misconfigured anything — you need API credits on the developer platform. Buy them at platform.tripo3d.ai, not in the Studio app.
Capping what can be spent
Set ASSET_SPEND_LIMIT_CENTS and every credit-consuming tool checks it before contacting the provider at all — including before a mesh or reference image is uploaded — refusing with the remaining balance named rather than overspending. The ceiling is in US cents because the two providers bill in different units — Tripo in $0.01 credits, Leonardo in USD — and a limit mixing them would mean nothing.
Where a provider publishes a per-call price we use it. Where it does not, the guard uses a deliberately pessimistic placeholder and get_spend_report says which figures are which. It is a guard, not an invoice: real charges should come in at or under the estimate, never above it.
One provider is enough
Credentials are validated lazily, at the moment a tool needs them, never at startup. If you only set TRIPO_API_KEY, the server starts fine and every 3D tool works; the image tools return a clear CONFIG_MISSING error naming the variable you are missing. The reverse holds too. You are never forced to hold an account you do not want just to use the half of the pipeline you do.
MCP client setup
Claude Code / Claude Desktop
Add to your MCP configuration (claude_desktop_config.json, or .mcp.json in a project for Claude Code):
{
"mcpServers": {
"game-asset": {
"command": "node",
"args": ["/absolute/path/to/game-asset-mcp/dist/server.js"],
"env": {
"TRIPO_API_KEY": "tsk_...",
"LEONARDO_API_KEY": "...",
"ASSET_OUTPUT_DIR": "/absolute/path/to/your/project/assets/generated",
"ASSET_LOG_LEVEL": "info"
}
}
}
}
Use an absolute path for args and for ASSET_OUTPUT_DIR. An MCP client's working directory is not the one you think it is, and a relative output directory will scatter assets somewhere surprising.
Any other MCP client
The same server, described generically — a stdio child process:
{
"name": "game-asset",
"transport": "stdio",
"command": "npx",
"args": ["-y", "github:theisegoria/game-asset-mcp"],
"env": {
"TRIPO_API_KEY": "tsk_...",
"LEONARDO_API_KEY": "...",
"ASSET_OUTPUT_DIR": "/absolute/path/to/assets/generated"
}
}
Available tools
| Tool | Spends credits | What it does |
|---|---|---|
preview_asset_prompt |
No | Dry run. Shows the exact prompt and negative prompt a spec would produce, so art direction can be corrected before anything is paid for. |
generate_asset_reference |
Yes | Turns an asset spec into reference images built for reconstruction — isolated subject, whole silhouette, flat light, plain background. Creates the asset job. |
generate_reference_variations |
Yes | Explores one axis (silhouette, material treatment, detailing, wear, proportions, functional components) while holding the object's identity fixed. |
select_reference |
No | Marks which reference candidate the 3D step will reconstruct. Local bookkeeping only. |
create_3d_asset |
Yes | Reconstructs a mesh with PBR textures from the selected reference — or straight from text when no reference exists. Returns immediately with a job to poll. |
texture_existing_asset |
Yes | Applies new PBR materials to a mesh you already own (GLB/GLTF/FBX/OBJ/STL) or to a previously generated one. Geometry is untouched. |
get_asset_job |
No | Polls a job. Maps the provider's status vocabulary onto one normalized lifecycle and keeps the raw status alongside. |
download_asset |
No | Fetches the provider's model, textures and preview renders into your workspace, hashing and recording each file. |
inspect_asset |
No | Reads a downloaded glTF/GLB and reports what is actually in it — meshes, materials, texture channels, sizes. |
extract_pbr_trio |
No | Splits a glTF material into independent albedo, normal and roughness images, de-packing metallicRoughness (roughness = green, metallic = blue). Resamples to an exact size, averaging colour in linear light and data channels directly. |
normalize_mesh |
No | Repairs a mesh so it can be used: generates UVs for objects that have none (the usual reason a mesh cannot be textured), welds coincident vertices, dissolves degenerate triangles, names every material and forces opaque blending. Optional Blender dependency. |
generate_sound_effect |
Yes | Generates a short game sound effect from a description — impacts, weapon reports, UI blips, or a seamless ambience loop. Polls and downloads inline. |
create_game_prop |
Yes — images only | The intention-shaped entry point: plain-language request in, asset spec plus reference candidates out. Deliberately stops before the 3D spend so a human or agent picks the reference first. |
list_asset_jobs |
No | Lists known jobs, newest first, as compact summaries. |
rig_asset |
Yes | Builds a skeleton and skin weights for a generated asset so it can be animated. |
animate_asset |
Yes | Retargets a preset animation onto an asset that has already been rigged. Refuses an unrigged source rather than billing for nothing. |
retopologize_asset |
Yes | Rebuilds topology, quads by default — quads survive downstream editing and mesh qualification far better than generator triangle soup. |
validate_game_asset |
No | Judges a mesh against a shipping policy and returns pass/fail with per-check reasons — UVs, normals, tangents, triangle budget, materials, texture resolution, bounding-box sanity. Every threshold overridable. |
batch_prepare_meshes |
No | Runs validate → normalize → validate across a list of .glb/.gltf paths (up to 500) and returns a per-item verdict. Meshes that already pass are left untouched; one bad file is reported against its own item and never stops the run. |
get_spend_report |
No | What this workspace has spent, by tool, with remaining headroom — and whether each figure is a published price or a pessimistic placeholder. |
Only nine tools can cost you money, and each one says so in its description before it is called.
The free local half (no API keys, no network)
Eleven of the twenty tools never spend a credit, and only two of those eleven use the network at all — get_asset_job polls, and download_asset fetches; both are free but are network calls. The other nine work offline. The five below are the mesh pipeline, and if you already have meshes they are the whole product.
| Tool | What it answers |
|---|---|
inspect_asset |
What is actually inside this glTF? Meshes, materials, texture channels, sizes, bounds. |
validate_game_asset |
Is this shippable? Pass/fail with per-check reasons and every threshold overridable. |
normalize_mesh |
Repair it: generate UVs for objects that have none, weld coincident vertices, dissolve degenerate triangles, name materials. |
batch_prepare_meshes |
The same, across a list of .glb/.gltf paths, with a per-item verdict. A failed item can still have written a file — when normalization succeeds but the result misses the policy, the mesh is kept for inspection. Use outputsWritten, not prepared, to predict the file count. |
extract_pbr_trio |
Split a material into albedo / normal / roughness images, de-packing metallicRoughness correctly. |
The usual loop is validate → normalize → validate again, so the repair is proven rather than assumed:
validate_game_asset modelPath=/art/crate.glb
→ fails: uvs_present ("nothing can texture this")
normalize_mesh modelPath=/art/crate.glb outputDir=/art/out
→ objectsUnwrapped=2, triangles 3183 → 1750
validate_game_asset modelPath=/art/out/crate_normalized.glb
→ passes
batch_prepare_meshes runs that loop over a list and reports each item separately. Meshes that already pass are left untouched rather than rewritten, one bad file never stops the run, and two sources that share a basename get distinct outputs instead of overwriting each other.
Missing UVs is the defect worth knowing about. A mesh with no UV coordinates cannot be textured by anything — not this tool, not a provider, not you by hand. Generators and marketplace assets ship without them routinely. validate_game_asset names it first for that reason.
Normalization needs Blender (4.x+). Without it the tools still validate and report; they simply cannot repair. On macOS Blender is not on PATH, so either set BLENDER_PATH or rely on the bundled /Applications/Blender.app default.
Example workflow
Full pipeline: idea to inspected asset
1. generate_asset_reference → spends image credits, returns assetJobId + N candidates
2. (inspect the images) → look at the returned reference images and choose one
3. select_reference → free; records which candidate wins
4. create_3d_asset → spends 3D credits, returns a task to poll
5. get_asset_job → free; poll until status is "ready" (or "failed")
6. download_asset → free; pulls model + textures + previews into the workspace
7. inspect_asset → free; confirms what actually landed on disk
Step 2 is not decoration. Picking the reference before spending 3D credits is the whole reason the pipeline is split here: a bad reference produces a melted mesh, and you only discover that after paying for the reconstruction.
Retexture: shorter, cheaper, and the flow most tools do not have
You already have the mesh. There is nothing to reference, nothing to select, nothing to reconstruct:
1. texture_existing_asset → spends texturing credits on a mesh you supply
2. get_asset_job → free; poll until ready
3. download_asset → free
4. inspect_asset → free
One paid call instead of two, and the geometry you already approved comes back unchanged.
Costs and side effects
Calls that spend provider credits: generate_asset_reference, generate_reference_variations, create_3d_asset, texture_existing_asset, generate_sound_effect, rig_asset, animate_asset, retopologize_asset, and the image-generation step inside create_game_prop. Nothing else in this server can be charged for.
Calls that are free: select_reference, get_asset_job, download_asset, inspect_asset, list_asset_jobs, preview_asset_prompt, extract_pbr_trio, normalize_mesh, validate_game_asset, batch_prepare_meshes, get_spend_report. Poll, inspect, split and download as often as you like.
A credit-consuming POST is never retried automatically. This is a deliberate, load-bearing rule and it lives in the HTTP layer, not in each call site. When a request that creates a generation task fails — timeout, socket reset, 502 — the client cannot tell whether the provider accepted it before the connection broke. Retrying might be free; it might also double-charge you for a mesh you never receive. So it does not retry, the error comes straight back, and the decision to try again is yours. Idempotent reads — status polls, file downloads — retry freely with backoff, because they cost nothing to repeat.
Other side effects worth knowing:
- Files are written to disk. Downloaded assets land under
ASSET_OUTPUT_DIR, and a download path that escapes the workspace root is refused. Three tools are different and deliberately so:extract_pbr_trio,normalize_meshandbatch_prepare_mesheswrite where you tell them to, including outside the workspace, because they operate on meshes you already own and those do not live in an asset-generation directory. Give them a destination you meant. download_assetandgenerate_sound_effectaccept adestinationthat overridesASSET_OUTPUT_DIRfor that one call. It is still contained: a path escaping the given root is refused.ASSET_OUTPUT_DIRshould be absolute. A relative value resolves against the server's working directory, which your MCP client chooses — several spawn from/. The server refuses to start with a message naming the resolved path and the working directory it came from. That diagnosis covers the eight errnos this can realistically produce — ENOENT, EACCES, EPERM, EROFS, ENOTDIR, ELOOP, ENAMETOOLONG and ENOSPC, including ASSET_OUTPUT_DIR pointing at a file rather than a directory. Anything else still propagates raw.- Nothing is silently overwritten. A derived output name gets a numeric suffix (
crate,crate_2, …) rather than destroying a result you may already have reviewed, and the name is claimed by exclusive create so two items in one batch cannot race for it. An explicitoutputPathis refused outright if a file is already there, unless you passoverwrite: true— and it is refused unconditionally, with no opt-out, if it resolves onto the input mesh. That resolution accounts for symlinks, hardlinks, case-insensitive volumes, and the exporter's habit of rewriting the extension, because every one of those has destroyed a source mesh here. - Downloads are capped at
ASSET_MAX_DOWNLOAD_BYTESand the cap is enforced while streaming, not from theContent-Lengthheader — a server that lies about the size cannot exhaust your memory. - Only HTTPS. Non-HTTPS URLs are refused outright, including ones that arrive inside a provider's response.
- API keys are redacted from logs centrally, so no individual log call site can leak one.
Workspace layout
Every asset gets a self-contained directory. Open it in a file browser six months later and it still explains itself:
assets/generated/
├── .jobs/ job records, one JSON file per job
│ └── asset_<uuid>.json
└── <asset_name>/
├── asset.json complete provenance: spec, prompt, seed,
│ model version, provider ids, file hashes
├── source/ the reference image(s) the mesh was built from
├── model/ the mesh (GLB by default)
├── textures/ extracted PBR maps
├── previews/ provider-rendered turnarounds
└── metadata/ raw provider payloads, kept for debugging
<asset_name> is your spec's name, sanitized: lowercased, non-alphanumerics collapsed to underscores. The .jobs directory is a dot-directory on purpose — browsing your asset workspace should show assets, not bookkeeping.
Troubleshooting
Every error carries a machine-readable error field naming the class, plus a retryable flag, so an agent can decide what to do next without parsing prose. The names below are the values of that error field.
The server starts and immediately exits — the client says only "connection closed".
Three known causes, and the server now names the first two itself rather than dying silently.
- A relative
ASSET_OUTPUT_DIR. It resolves against the server's working directory, which your MCP client chooses — several spawn from/, whereassets/generatedbecomes/assetsand cannot be created. Use an absolute path. The refusal names the resolved path and the working directory it came from. - A workspace the process cannot write to. Same refusal, different errno.
- A stale build. If
dist/predates a change to the entry point, rebuild.npm run verifybuilds and then completes a real MCP handshake, which is the fastest way to tell a broken server from a broken client config.
normalize_mesh or batch_prepare_meshes refuses with "Blender not found".
There is no local Blender on PATH. On macOS the app bundle is not on PATH even when Blender is installed — set BLENDER_PATH to the executable inside the bundle. batch_prepare_meshes degrades rather than failing: it still validates every mesh and reports what would need repairing.
CONFIG_MISSING — missing credential.
The tool you called needs a provider you have not configured. The message names the exact environment variable. Set it in your MCP client's env block and restart the client. A .env file is never read: there is no dotenv dependency, so the variable must be exported by whatever launches the server.
PROVIDER_HTTP with status 401/403 — invalid API key.
The key is wrong, revoked, or the wrong provider's. Two specific traps: Leonardo keys need API access enabled on the account (a web login alone does not grant it), and a Tripo key with no API credit balance can fail on the first paid call even though the key itself is valid. See the credits warning above.
RATE_LIMITED — HTTP 429.
Marked retryable. Polls back off and retry automatically (400 ms, 800 ms, 1600 ms, capped at 8 s). Downloads do not retry — download_asset streams in one attempt, so re-issue it yourself; because provider URLs expire, re-poll with get_asset_job first rather than retrying a stale URL. Generation requests do not retry either, deliberately, because they cost money. A 429 during a download surfaces as PROVIDER_HTTP with status 429, not as RATE_LIMITED.
PROVIDER_TASK_FAILED — the task failed provider-side.
The HTTP call succeeded and the generation did not. The provider's own message is preserved in the error details. A moderation refusal also lands here: rewrite the prompt rather than retrying it unchanged. Note that a Tripo response can carry HTTP 200 with a non-zero envelope code; that is a failure, and this server treats it as one instead of reporting a phantom success.
Download fails with PROVIDER_HTTP 403/404 — the URL expired.
This is the single most common surprise. Provider model and preview URLs are short-lived. They are signed, they expire, and a URL that worked twenty minutes ago is now dead. The fix is not to retry the same URL — call get_asset_job again to re-poll the provider for fresh URLs, then download_asset immediately. As a habit: download as soon as a job reports ready, not at the end of a long session.
INVALID_INPUT — unsupported image format.
Reference images should be standard web-safe raster formats (PNG, JPEG, WebP). HDR, EXR, layered PSD, SVG and multi-page TIFF are not reconstructable inputs. For texture_existing_asset, meshes must be GLB, GLTF, FBX, OBJ or STL. Convert first; the provider will not do it for you.
PROVIDER_MALFORMED_RESPONSE — the provider returned something unexpected.
Non-JSON body, an empty envelope, a success with no data, or an upload that returned no file token. Usually means a provider-side incident or an API version drift. Set ASSET_LOG_LEVEL=debug to see the request shape (keys are redacted), and check the provider's status page before assuming the bug is local.
DOWNLOAD_TOO_LARGE.
The file exceeded ASSET_MAX_DOWNLOAD_BYTES. A high-quality PBR GLB can be large; raise the limit if you genuinely want the file.
PATH_ESCAPE.
A provider-supplied filename tried to resolve outside your workspace. The write was refused. This should never happen in normal operation — please open an issue if it does.
Status
This is early software, and the parts most likely to drift are marked as such rather than quietly assumed.
Tripo's v3 endpoint paths are pinned in exactly one module (src/providers/model3d/tripo.ts) and documented in a comment at the top of it. Tripo's public docs describe the v3 surface two different ways — a generic task endpoint and per-operation paths — and both appear in current documentation. This client implements the task form, which matches the observable behaviour that every generation returns a task_id to poll, and exposes TRIPO_BASE_URL so you can retarget without editing code. If they are wrong you will see a 404 that looks exactly like a bad API key, so check the path before the key.
No call has ever been made to a live provider API. This is the most important caveat here, so it is stated plainly rather than buried. Every one of the 322 tests runs against mocks or the local filesystem. They cover prompt construction, status mapping, path safety, the job store, the HTTP layer's retry and redirect rules, and glTF inspection against real files — but a green suite says nothing about whether Leonardo and Tripo behave the way this client assumes.
Concretely, these remain unverified:
- The Tripo v3 endpoint paths described above.
- Whether
texture_modelaccepts an uploaded mesh (file_token) or only a mesh produced by a prior Tripo task (original_model_task_id). This decides whether you can retexture a model you already own, which is the feature this server exists for. Resolving it costs one HD texture call. - Sound-effect generation is unverified. Leonardo documents the Sound Effects v2 request
contract (model,prompt,duration1-22s,prompt_influence,loop,quantity) but not
its response shape or how the finished audio is retrieved. The client reads the generation id
and audio URLs from several plausible shapes and throws with the raw payload attached when none
match, rather than reporting an empty success. Expect the first real call to need a fix, and
please open an issue with the payload shape you saw. - The Leonardo model ids in
src/providers/image/leonardo.ts, which were transcribed from published documentation. Check them againstGET /platformModels; a stale id fails as an HTTP 400 that reads like a malformed request body. BothLEONARDO_MODEL_IDand a per-callmodelIdexist as escape hatches.
If you are the first person to run this with real keys, expect to fix an endpoint path, and please open an issue with what you found.
What is verified: npm run verify builds the server, starts it over stdio with a real MCP client, completes the handshake and asserts all twenty tools register. That is a protocol round-trip, not a version string — a server that fails to register its tools still starts perfectly happily.
Parts of the local pipeline — inspect_asset, extract_pbr_trio, normalize_mesh, validate_game_asset — are additionally checked against real shipped game assets rather than fixtures, because a synthetic fixture and the parser that reads it can share the same mistake and both look green. It has happened here: a wrong glTF magic constant survived a full synthetic suite and was caught only by a real file. The UV-less mesh they use is committed here rather than read from a sibling checkout. It used to be read live from the game repo, and when that mesh was repaired these tests went red for a change that was entirely correct — an assertion pinning a fact about a file this project does not control. A test may not depend on content it does not own.
One test spawns the built server through a symlinked bin — what node_modules/.bin actually contains — and speaks MCP to it, because that is where the entry-point guard failed: the server exited instantly on every install while passing every other test. It symlinks rather than installing, so it cannot catch a packaging regression in files or prepare; a real npm install from GitHub is still a manual check.
Contributing
Issues and pull requests welcome. If you add a provider, implement ImageProvider or Model3DProvider and change nothing else — if a new provider forces a change to the tool surface, the abstraction is wrong and that is the bug worth discussing first.
License
MIT © 2026 Ben Haire. See LICENSE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found